#!/usr/bin/env bash
set -euo pipefail

# This script is copied in from <1.0 Umbrel and modified to work with umbreld.
# It provides a compatibility layer so the old app system works reliably.

# TODO: Hardcoding these for now since they used to be read from .env
# Figure out what other stuff we need from the old .env and how to pass it through.
export NETWORK_IP='10.21.0.0'
export GATEWAY_IP='10.21.0.1'
export AUTH_PORT='2000'
export UMBREL_AUTH_SECRET='DEADBEEF'
export MANAGER_IP="10.21.21.4"

VERSION="0.0.3"

UMBREL_ROOT="${SCRIPT_UMBREL_ROOT}"
USER_FILE="${UMBREL_ROOT}/db/user.json"

CURRENT_SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"

APP_PROXY_SERVICE_NAME="app_proxy"
# We'll pass this in from umbreld
# REMOTE_TOR_ACCESS="false"
# if [[ -f "${USER_FILE}" ]]; then
#   REMOTE_TOR_ACCESS=$(cat "${USER_FILE}" | jq 'has("remoteTorAccess") and .remoteTorAccess')
# fi

show_help() {
  cat << EOF
CLI (v${VERSION}) for managing Umbrel apps

Usage: app <command> <app> [<arguments>]

Commands:
    install                     Pulls down images for an app and starts it
    uninstall                   Removes images and destroys all data for an app
    reinstall                   Calls 'uninstall', followed by 'install' for an app
    start                       Starts an installed app
    stop                        Stops an installed app
    restart                     Restarts an installed app
    compose                     Passes all arguments to docker-compose
    ls-installed                Lists installed apps
    ls-dependencies             Lists dependencies of an app
    ls-transitive-dependencies  Lists transitive dependencies of an app
EOF
}

# Use GNU cp on macos
if [[ "$(uname)" = "Darwin" ]]; then
    cp="gcp"
else
    cp="cp"
fi

check_dependencies () {
  for cmd in "$@"; do
    if ! command -v $cmd >/dev/null 2>&1; then
      >&2 echo "This script requires \"${cmd}\" to be installed"
      exit 1
    fi
  done
}

list_installed_apps() {
  # cat "${USER_FILE}" 2> /dev/null | jq -r 'if has("installedApps") then .installedApps else [] end | join("\n")' || true
  local yaml_file="${UMBREL_ROOT}/umbrel.yaml"
  yq e '.apps[]' "${yaml_file}" 2> /dev/null || true
}

list_dependencies_of() {
  local app="$1"
  local app_data_dir="${UMBREL_ROOT}/app-data/${app}"
  local app_manifest_file="${app_data_dir}/umbrel-app.yml"
  local app_settings_file="${app_data_dir}/settings.yml"

  # Get the app's dependencies and substitute with alternatives if present
  local dependencies=$(yq e '.dependencies[]' "${app_manifest_file}" 2> /dev/null || true)
  for dep in $dependencies; do
    yq e ".dependencies.${dep} // \"${dep}\"" "${app_settings_file}" 2> /dev/null || echo "${dep}"
  done
}

list_transitive_dependencies_of() {
  local app="$1"
  local -A processed=()
  local -A processing=()
  local output=()

  process_dependencies_in_post_order() {
    local current_app="$1"

    # Skip if already processed
    if [[ -n "${processed[$current_app]:-}" ]]; then
      return
    fi

    # Skip on circular dependencies
    if [[ -n "${processing[$current_app]:-}" ]]; then
      return 1
    fi

    processing["$current_app"]=1
    local deps=($(list_dependencies_of "$current_app"))

    # Process dependencies before processing the app itself
    for dep in "${deps[@]}"; do
      if ! process_dependencies_in_post_order "$dep"; then
        echo "Error: Circular dependency in app $app: $current_app -> $dep" >&2
        processing["$current_app"]=0
        return 1
      fi
    done

    processed["$current_app"]=1
    processing["$current_app"]=0
    output+=("$current_app")
  }

  process_dependencies_in_post_order "$app"

  # Omit the app itself
  for ((i = 0; i < ${#output[@]} - 1; i++)); do
    echo "${output[i]}"
  done
}

# Deterministically derives 128 bits of cryptographically secure entropy
derive_entropy () {
  # Make sure we use the seed from the real Umbrel installation if this is
  # an OTA update.
  SEED_FILE="${UMBREL_ROOT}/db/umbrel-seed/seed"
  if [[ ! -f "${SEED_FILE}" ]] && [[ -f "${UMBREL_ROOT}/../.umbrel" ]]; then
    SEED_FILE="${UMBREL_ROOT}/../db/umbrel-seed/seed"
  fi

  identifier="${1}"
  umbrel_seed=$(cat "${SEED_FILE}") || true

  if [[ -z "$umbrel_seed" ]] || [[ -z "$identifier" ]]; then
    >&2 echo "Missing derivation parameter, this is unsafe, exiting."
    exit 1
  fi

  # We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl
  printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${umbrel_seed}" | sed 's/^.* //'
}

# Setup env. for this context for a given app
source_app() {
  local -r app="${1}"

  local -r app_domain="$(hostname -s 2>/dev/null || echo "umbrel").local"
  local -r app_entropy_identifier="app-${app}-seed"

  # Load in existing Umbrel .env
  # So that apps in their exports.sh can access
  # e.g. $TOR_PROXY_IP, $TOR_PROXY_PORT
  # [[ -f "${UMBREL_ROOT}/.env" ]] && . "${UMBREL_ROOT}/.env"

  export NETWORK_IP="${NETWORK_IP}"

  # Set other useful vars. used in exports
  export DEVICE_HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "umbrel")"
  export DEVICE_DOMAIN_NAME="${DEVICE_HOSTNAME}.local"

  # Set env using all transitive dependencies exports.sh
  # Do this first so that no app exports can
  # Override any app specific exports defined below
  EXPORTS_TOR_DATA_DIR="${UMBREL_ROOT}/tor/data"

  APPS_TO_SOURCE="$(list_transitive_dependencies_of "$app")"

  # $app might not be in the 'installed apps list' yet
  # i.e. If it is currently being installed
  # So we'll add it to the list of apps that will be 'sourced'
  if ! echo "${APPS_TO_SOURCE}" | grep --quiet "^${app}$"; then
    APPS_TO_SOURCE="${APPS_TO_SOURCE}"$'\n'"${app}"
  fi

  for EXPORTS_APP_ID in $APPS_TO_SOURCE; do
    EXPORTS_APP_DIR="${UMBREL_ROOT}/app-data/${EXPORTS_APP_ID}"
    EXPORTS_APP_FILE="${EXPORTS_APP_DIR}/exports.sh"
    EXPORTS_APP_DATA_DIR="${EXPORTS_APP_DIR}/data"

    if [[ -f "${EXPORTS_APP_FILE}" ]]; then
      # We replace the literal text "${UMBREL_ROOT}/scripts/app" within the exports.sh file with the path to this script
      sed -i 's|"${UMBREL_ROOT}/scripts/app"|'"${CURRENT_SCRIPT_PATH}"'|g' "${EXPORTS_APP_FILE}"

      # We replace the literal text "${UMBREL_ROOT}/db/user.json" within the exports.sh file with the path to the umbrel.yaml file
      # This specifically handles the Tailscale app
      sed -i 's|"${UMBREL_ROOT}/db/user.json"|"${UMBREL_ROOT}/umbrel.yaml"|g' "${EXPORTS_APP_FILE}"

      # Source the modified temporary exports file
      . "${EXPORTS_APP_FILE}"
    fi
  done

  # App specific exports
  export APP_ID="${app}"
  export APP_MANIFEST_FILE="${app_data_dir}/umbrel-app.yml"
  export APP_VERSION=$(cat "${APP_MANIFEST_FILE}" | yq '.version')
  
  # This provides the app proxy with context of the app
  export APP_PROXY_HOSTNAME="app_proxy_${app}"
  export APP_PROXY_PORT=$(cat "${APP_MANIFEST_FILE}" | yq '.port')
  
  export APP_DATA_DIR="${app_data_dir}"
  export APP_DOMAIN="${app_domain}"
  export APP_HIDDEN_SERVICE="not-enabled.onion"
  if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
    export APP_HIDDEN_SERVICE="$(cat "${app_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")"
  fi
  export APP_SEED=$(derive_entropy "${app_entropy_identifier}")
  export APP_PASSWORD=$(derive_entropy "${app_entropy_identifier}-APP_PASSWORD")

  # Tor specific exports
  export TOR_DATA_DIR="${UMBREL_ROOT}/tor/data"
  export TOR_ENTRYPOINT_SCRIPT="${SCRIPT_DOCKER_FRAGMENTS}/tor-entrypoint.sh"
  export TOR_HS_APP_DIR="/data/app-${app}"
  export TOR_HS_PORTS="80:${APP_PROXY_HOSTNAME}:${APP_PROXY_PORT}"

  # TODO: Look into why this needed to be commented out for Mark.
  # tor_extra_hs_varname=$(echo "APP_${APP_ID^^}_TOR_HS_EXTRA_PORTS" | tr '-' '_')
  # tor_hs_extra_ports="${!tor_extra_hs_varname:-}"

  # if [[ ! -z "${tor_hs_extra_ports}" ]]; then
  #   export TOR_HS_PORTS="${TOR_HS_PORTS} ${tor_hs_extra_ports}"
  # fi

  # Other
  export UMBREL_ROOT
}

# Check dependencies
check_dependencies docker jq yq openssl envsubst

if [ -z ${1+x} ]; then
  command=""
else
  command="$1"
fi

# Lists installed apps
if [[ "$command" = "ls-installed" ]]; then
  list_installed_apps

  exit
fi

if [ -z ${2+x} ]; then
  show_help
  exit 1
else
  app="$2"

  # Lists (transitive) dependencies
  if [[ "$command" = "ls-dependencies" ]]; then
    list_dependencies_of "$app"
    exit
  elif [[ "$command" = "ls-transitive-dependencies" ]]; then
    list_transitive_dependencies_of "$app"
    exit
  fi

  # repo=$(cat "${USER_FILE}" 2> /dev/null | jq -r ".appOrigin.\"${app}\"" || true)
  # repo_path=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}")
  # app_repo_dir="${repo_path}/${app}"
  app_repo_dir="${SCRIPT_APP_REPO_DIR}"
  app_data_dir="${UMBREL_ROOT}/app-data/${app}"

  app_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}/hostname"
  
  if [[ "${app}" == "installed" ]]; then
    for app in $(list_installed_apps); do
      if [[ "${app}" != "" ]]; then
        "${0}" "${1}" "${app}" "${@:3}" &
      fi
    done
    wait
    exit
  fi
  
  if [[ -z "${app}" ]]; then
    >&2 echo "Error: \"${app}\" is not a valid app"
    exit 1
  fi
fi

if [ -z ${3+x} ]; then
  args=""
else
  args="${@:3}"
fi

execute_hook() {
  local -r app="${1}"
  local -r name="${2}"

  local -r app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks"
  local -r hook="${app_hooks_dir}/${name}"

  if [[ -x "${hook}" ]]; then
    echo "Executing hook: ${hook}"

    # We replace the literal text "${UMBREL_ROOT}/scripts/app" within the hook file with the path to this script
    sed -i 's|"${UMBREL_ROOT}/scripts/app"|'"${CURRENT_SCRIPT_PATH}"'|g' "${hook}"

    # Swallow non-zero exit code
    "${hook}" || true
  fi
}

compose() {
  local -r app="${1}"
  shift

  # Source env.
  source_app "${app}"

  # Define support compose files
  local -r app_proxy_compose_file="${SCRIPT_DOCKER_FRAGMENTS}/docker-compose.app_proxy.yml"
  local -r tor_compose_file="${SCRIPT_DOCKER_FRAGMENTS}/docker-compose.tor.yml"
  local -r common_compose_file="${SCRIPT_DOCKER_FRAGMENTS}/docker-compose.common.yml"
  local -r app_compose_file="${app_data_dir}/docker-compose.yml"

  local -r umbrel_env_file="${UMBREL_ROOT}/.env"

  # We need to use the proxy compose file first
  # To allow vars. in the app's compose file to override variables
  compose_files=()

  # Detect if the 'app_proxy' service has been defined
  # In the app's docker-compose file
  has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")")

  if [[ "${has_app_proxy_service}" == "true" ]]; then
    compose_files+=( "--file" "${app_proxy_compose_file}" )
  fi

  # If remote Tor access is enabled
  # Then include a compose file for Tor
  if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
    compose_files+=( "--file" "${tor_compose_file}" )
  fi

  # Add app's compose file last so that it can override
  # Any of the other compose files
  compose_files+=( "--file" "${common_compose_file}" )
  compose_files+=( "--file" "${app_compose_file}" )

  # Merge compose files and args. passed into 'compose'
  compose_args=("${compose_files[@]}" "${@}")

  # TODO: We removed the .env source, we probs need to add that back in somehow
  # --env-file "${umbrel_env_file}" \
  docker compose \
    --project-name "${app}" \
    "${compose_args[@]}"
}

update_installed_apps() {
  local -r action="${1}"
  local -r app="${2}"
  local -r repo="${3:-null}"

  while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
    echo "Waiting for JSON lock to be released for ${app} update..."
    sleep 1
  done
  # This will cause the lock-file to be deleted in case of a
  # premature exit.
  trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT

  [[ "${action}" == "add" ]] && operator="+" || operator="-"
  updated_json=$(cat "${USER_FILE}" | jq ".installedApps |= (. ${operator} [\"${app}\"] | unique)")
  echo "${updated_json}" > "${USER_FILE}"

  if [[ "${action}" == "add" ]]; then
    updated_json=$(cat "${USER_FILE}" | jq ".appOrigin |= (. ${operator} {\"${app}\":\"${repo}\"})")
  else
    updated_json=$(cat "${USER_FILE}" | jq "del(.appOrigin.\"${app}\")")
  fi
  echo "${updated_json}" > "${USER_FILE}"

  rm -f "${USER_FILE}.lock"
}

template_app() {
  local -r app="${1}"

  # Loop over all templates within app and populate them
  APP_TEMPLATE_FILES="${app_data_dir}/*.template"
  
  shopt -s nullglob
  for APP_TEMPLATE_INPUT_FILE in $APP_TEMPLATE_FILES; do
    # Output filename is the same as input with .template stripped off
    APP_TEMPLATE_OUTPUT_FILE="${APP_TEMPLATE_INPUT_FILE%.*}"

    # First we'll copy the file so we ensure the output
    # has the same fs permissions as the input
    $cp --archive "${APP_TEMPLATE_INPUT_FILE}" "${APP_TEMPLATE_OUTPUT_FILE}"
    cat "${APP_TEMPLATE_INPUT_FILE}" | envsubst > "${APP_TEMPLATE_OUTPUT_FILE}"
  done
}

copy_app_files() {
  local -r files_to_copy="${1}"

  for filename in $files_to_copy; do
    APP_FILES="${app_repo_dir}/${filename}"

    for app_file in $APP_FILES; do
      if [[ -f "${app_file}" ]] || [[ -d "${app_file}" ]]; then
        $cp --archive "${app_file}" "${app_data_dir}"
      fi
    done
  done
}

wait_for_tor_hs() {
  local -r app="${1}"
  
  # Check if the app's hidden service hostname
  # Has been already generated and exit early
  if [[ -f "${app_hidden_service_file}" ]]; then
    return
  fi

  # Check that the app has the App Proxy service defined
  local -r app_compose_file="${app_data_dir}/docker-compose.yml"
  has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")")

  if [[ "${has_app_proxy_service}" == "false" ]]; then
    echo
    >&2 echo "Warning: \"${app}\" has no '${APP_PROXY_SERVICE_NAME}' defined"
    >&2 echo "         \"${app}\" needs this to generate Tor HS"
    echo
    return
  fi

  # If a tor service will start
  # and there is no existing tor hs hostname
  # Let's allow 10 seconds to generate it and then start the app
  if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
    echo "Generating hidden services for ${app}..."
    # We must first start the App Proxy
    # So that it's hostname is resolvable by Tor
    # More details here: https://github.com/torproject/tor/blob/01bda6c23f58947ad1e20ea6367a5c260f53dfab/src/feature/hs/hs_common.c#L743
    # And here: https://github.com/torproject/tor/blob/22552ad88e1e95ef9d2c6655c7602b7b25836075/src/lib/net/resolve.c#L297
    # Otherwise Tor will throw this error:
    # Unparseable address in hidden service port configuration.
    compose "${app}" up --detach app_proxy
    compose "${app}" up --detach tor_server

    for attempt in $(seq 1 100); do
      if [[ -f "${app_hidden_service_file}" ]]; then
        echo "Hidden service file created successfully!"
        break
      fi
      sleep 0.1
    done

    if [[ ! -f "${app_hidden_service_file}" ]]; then
      echo "Hidden service file wasn't created"
    fi
  fi
}

start_app() {
  local -r app="${1}"

  # Source env.
  source_app "${app}"

  # Now apply templates
  template_app "${app}"

  # Wait for Tor's HS hostname to exist
  wait_for_tor_hs "${app}"

  execute_hook "${app}" "pre-start"

  # Start all the app's containers
  compose "${app}" up --detach --build

  execute_hook "${app}" "post-start"
}

# Check that the app is installed
must_be_installed_guard() {
  if ! list_installed_apps | grep --quiet "^${app}$"; then
    >&2 echo "Error: app \"${app}\" is not installed yet"
    exit 1
  fi
}

# Pulls down images for an app and starts it
if [[ "$command" = "install" ]]; then

#   repo=$("${UMBREL_ROOT}/scripts/repo" "locate" "${app}")

#   if [[ -z "${repo}" ]]; then
    # >&2 echo "Error: \"${app}\" not found in any local app repo"
    # exit 1
#   fi

#   app_repo_dir=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}")
#   app_repo_dir="${app_repo_dir}/${app}"

#   echo "Installing '${app}' from: ${repo}"

#   echo "Setting up data dir for app ${app}..."
#   mkdir -p "${app_data_dir}"

#   # Copy all app files
#   rsync --archive --verbose --exclude ".gitkeep" "${app_repo_dir}/." "${app_data_dir}"

  execute_hook "${app}" "pre-install"

  # Source env.
  source_app "${app}"

  # Now apply templates
  template_app "${app}"

  echo "Pulling images for app ${app}..."
  compose "${app}" pull

#   if [[ "$*" != *"--skip-start"* ]]; then
#     echo "Starting app ${app}..."
    start_app "${app}"
#   fi

#   echo "Saving app ${app} in DB..."
#   update_installed_apps add "${app}" "${repo}"

  execute_hook "${app}" "post-install"

  echo "Successfully installed app ${app}"
  exit
fi

# Removes images and destroys all data for an app
if [[ "$command" = "uninstall" ]]; then

  must_be_installed_guard

  execute_hook "${app}" "pre-uninstall"

  # If a post uninstal hook exists
  # Then make a copy before it's deleted below
  app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks"
  post_uninstall_app_hook="${app_hooks_dir}/post-uninstall"
  if [[ -x "${post_uninstall_app_hook}" ]]; then
    temp_post_uninstall_app_hook="/tmp/${app}-post-uninstall"

    $cp --archive "${post_uninstall_app_hook}" "${temp_post_uninstall_app_hook}"

    post_uninstall_app_hook="${temp_post_uninstall_app_hook}"
  else
    post_uninstall_app_hook=""
  fi

  echo "Removing images for app ${app}..."
  compose "${app}" down --rmi all --remove-orphans

  echo "Deleting app data for app ${app}..."
  if [[ -d "${app_data_dir}" ]]; then
    rm -rf "${app_data_dir}"
  fi

  echo "Removing app ${app} from DB..."
  update_installed_apps remove "${app}"

  if [[ ! -z "${post_uninstall_app_hook}" ]]; then
    "${post_uninstall_app_hook}" || true

    rm -rf "${post_uninstall_app_hook}"
  fi

  echo "Successfully uninstalled app ${app}"
  exit
fi

# Stops an installed app
if [[ "$command" = "stop" ]]; then

#   must_be_installed_guard

  execute_hook "${app}" "pre-stop"

  echo "Stopping app ${app}..."
  compose "${app}" rm --force --stop

  execute_hook "${app}" "post-stop"

  exit
fi

if [[ "$command" = "reinstall" ]]; then

  "${0}" "uninstall" "${app}"

  echo
  "${0}" "install" "${app}"

  exit
fi

# Starts an installed app
if [[ "$command" = "start" ]]; then

#   must_be_installed_guard

  echo "Starting app ${app}..."
  start_app "${app}"

  exit
fi

# Restarts an installed app
if [[ "$command" = "restart" ]]; then

  "${0}" "stop" "${app}"

  "${0}" "start" "${app}"

  exit
fi

# Get logs for an app
if [[ "$command" = "logs" ]]; then
  compose "${app}" logs --tail 500

  exit
fi

# Update an installed app
if [[ "$command" = "update" ]]; then

  # must_be_installed_guard

  # Check that the app folder still exists
  # Within the associated local app repo
  if [[ ! -d "${app_repo_dir}" ]]; then
    >&2 echo "Error: Local app repo no longer exists for ${app}"
    exit 1
  fi

  echo "Updating '${app}' from: ${app_repo_dir}"
  # Save current images to clean up later
  app_compose_file="${app_data_dir}/docker-compose.yml"
  app_old_images=$(yq e '.services | map(select(.image != null)) | .[].image' "${app_compose_file}")

  if [[ "$*" != *"--skip-stop"* ]]; then
    "${0}" "stop" "${app}"
  fi

  execute_hook "${app}" "pre-update"
  
  # App updates will only copy files from this whitelist:
  UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks"

  # We copy umbrel-app.yml after the app has started
  # That way the frontend knows the update has finished
  # And the app is running again
  UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"

  copy_app_files "${UPDATE_FILES_WHITELIST_PRE}"

  # Ensure remaining files are copied in case of unexpected exit
  trap "copy_app_files "${UPDATE_FILES_WHITELIST_POST}"; exit $?" INT TERM EXIT

  # Source env. after new exports.sh is copied (done above via 'copy_app_files')
  source_app "${app}"

  # Now apply templates
  template_app "${app}"

  echo "Pulling images for app ${app}..."
  compose "${app}" pull

  # Copy remaining files to mark update as complete
  copy_app_files "${UPDATE_FILES_WHITELIST_POST}"

  if [[ "$*" != *"--skip-start"* ]]; then
    "${0}" "start" "${app}"
    # Remove any old images we don't need anymore
    docker rmi $app_old_images || true
  fi

  execute_hook "${app}" "post-update"

  exit
fi

# Update an installed app
if [[ "$command" = "pre-patch-update" ]]; then

  # must_be_installed_guard

  # Check that the app folder still exists
  # Within the associated local app repo
  if [[ ! -d "${app_repo_dir}" ]]; then
    >&2 echo "Error: Local app repo no longer exists for ${app}"
    exit 1
  fi

  echo "Updating '${app}' from: ${app_repo_dir}"
  # Save current images to clean up later
  app_compose_file="${app_data_dir}/docker-compose.yml"
  app_old_images=$(yq e '.services | map(select(.image != null)) | .[].image' "${app_compose_file}")

  if [[ "$*" != *"--skip-stop"* ]]; then
    "${0}" "stop" "${app}"
  fi

  execute_hook "${app}" "pre-update"
  
  # App updates will only copy files from this whitelist:
  UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks"

  # We copy umbrel-app.yml after the app has started
  # That way the frontend knows the update has finished
  # And the app is running again
  UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"

  copy_app_files "${UPDATE_FILES_WHITELIST_PRE}"

  # Ensure remaining files are copied in case of unexpected exit
  trap "copy_app_files "${UPDATE_FILES_WHITELIST_POST}"; exit $?" INT TERM EXIT

  # Source env. after new exports.sh is copied (done above via 'copy_app_files')
  source_app "${app}"

  # Now apply templates
  template_app "${app}"

  # Copy remaining files to mark update as complete
  copy_app_files "${UPDATE_FILES_WHITELIST_POST}"

  exit
fi

# Update an installed app
if [[ "$command" = "post-patch-update" ]]; then

  echo "Pulling images for app ${app}..."
  compose "${app}" pull

  if [[ "$*" != *"--skip-start"* ]]; then
    "${0}" "start" "${app}"
    # Remove any old images we don't need anymore
    # docker rmi $app_old_images || true
  fi

  execute_hook "${app}" "post-update"

  exit
fi

# Nuke app images
if [[ "$command" = "nuke-images" ]]; then
  compose "${app}" down --rmi all --remove-orphans
  exit
fi

# Passes all arguments to docker-compose
if [[ "$command" = "compose" ]]; then

  compose "${app}" ${args}

  exit
fi

# If we get here it means no valid command was supplied
# Show help and exit
show_help
exit 1