#!/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.
# GATEWAY_IP is also baked into the MCP connect UI as the endpoint URL that
# agents installed as apps use to reach umbreld from inside their containers
# (INSTALLED_AGENT_MCP_URL in ui/src/routes/settings/mcp/agents.ts) — keep
# them in sync if this ever changes, e.g. when this script is refactored away.
export NETWORK_IP='10.21.0.0'
export GATEWAY_IP='10.21.0.1'

VERSION="0.0.3"

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

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

# Each shell owns its temporary files, including compose subshells used for
# bounded logs. An exports subshell must not remove its parent's source file.
_umbrel_runtime_temp_owner="${BASHPID}"
_umbrel_runtime_temp_files=()

_umbrel_runtime_cleanup_temp_files() {
  local -r status=$?
  if [[ "${BASHPID}" == "${_umbrel_runtime_temp_owner}" ]] && (( ${#_umbrel_runtime_temp_files[@]} > 0 )); then
    rm -f -- "${_umbrel_runtime_temp_files[@]}" || true
  fi
  return "${status}"
}

_umbrel_runtime_wrap_cleanup_trap() {
  local -r handler="${1}"
  local -r signal="${2}"
  if [[ -z "${handler}" && "${signal}" != EXIT ]]; then
    # Keep ignored signals ignored; the shell may still be sourcing a temp file.
    trap '' "${signal}"
  elif [[ "${handler}" == *'_umbrel_runtime_cleanup_temp_files'* ]]; then
    trap "${handler}" "${signal}"
  else
    # Evaluate the shell-quoted handler separately so empty actions, comments,
    # and trailing semicolons cannot break the surrounding cleanup wrapper.
    local quoted_handler
    printf -v quoted_handler '%q' "${handler}"
    # The if/else avoids errexit on a nonzero cleanup return while preserving
    # the original $? for the handler, including the legacy update handlers.
    trap 'if _umbrel_runtime_cleanup_temp_files; then eval '"${quoted_handler}"'; else eval '"${quoted_handler}"'; fi' "${signal}"
  fi
}

_umbrel_runtime_track_temp_file() {
  if [[ "${BASHPID}" != "${_umbrel_runtime_temp_owner}" ]]; then
    _umbrel_runtime_temp_owner="${BASHPID}"
    _umbrel_runtime_temp_files=()
  fi
  _umbrel_runtime_temp_files+=("${1}")
  local signal existing_trap
  for signal in EXIT INT TERM; do
    existing_trap="$(trap -p "${signal}")"
    if [[ -n "${existing_trap}" ]]; then
      # trap -p supplies shell-quoted arguments; forward them to our wrapper.
      eval "${existing_trap/#trap -- /_umbrel_runtime_wrap_cleanup_trap }"
    elif [[ "${signal}" == EXIT ]]; then
      _umbrel_runtime_wrap_cleanup_trap ':' EXIT
    else
      _umbrel_runtime_wrap_cleanup_trap "trap - ${signal}; kill -s ${signal} \"\${BASHPID}\"" "${signal}"
    fi
  done
}

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 containers 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/^.* //'
}

# Define the stable app paths before any lifecycle hook runs. APP_DATA_DIR is
# the fixed app definition directory; APP_DATA_ROOT is the app-owned persistent
# data root and defaults to its data/ child until storage relocation is enabled.
_umbrel_runtime_set_app_paths() {
  local -r app="${1}"
  local app_data_root
  app_data_root="$(printf '%s' "${SCRIPT_APP_DATA_ROOTS}" | jq --exit-status --raw-output --arg app "${app}" '.[$app]')"

  export APP_DATA_DIR="${UMBREL_ROOT}/app-data/${app}"
  export APP_DATA_ROOT="${app_data_root}"
}

# Setup env. for this context for a given app
source_app() {
  local -r app="${1}"
  local -r _umbrel_runtime_data_roots="${SCRIPT_APP_DATA_ROOTS}"
  local _umbrel_runtime_app_data_root
  _umbrel_runtime_app_data_root="$(printf '%s' "${_umbrel_runtime_data_roots}" | jq --exit-status --raw-output --arg app "${app}" '.[$app]')"

  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"
    # Keep the existing cross-version export name while resolving it to the
    # app's current physical data root on supported umbrelOS versions.
    EXPORTS_APP_DATA_DIR="$(printf '%s' "${_umbrel_runtime_data_roots}" | jq --exit-status --raw-output --arg app "${EXPORTS_APP_ID}" '.[$app]')"

    if [[ -f "${EXPORTS_APP_FILE}" ]]; then
      # Apply the legacy path rewrites to a temporary copy and source that, so
      # sourcing never writes to the installed exports.sh. Every dependent
      # sources a dependency's exports on start/stop/update, and rewriting the
      # file in place there raced the dependency's own update replacing it,
      # leaving an empty or truncated exports.sh behind. The copy lives next
      # to the original so exports that resolve paths relative to BASH_SOURCE
      # (e.g. peerswap, invoice-ninja) still see their own app directory.
      #
      # "${UMBREL_ROOT}/scripts/app" is rewritten to the path of this script and
      # "${UMBREL_ROOT}/db/user.json" to the umbrel.yaml file (for Tailscale).
      local exports_app_temp_file
      exports_app_temp_file="$(mktemp "${EXPORTS_APP_FILE}.tmp.XXXXXX")"
      _umbrel_runtime_track_temp_file "${exports_app_temp_file}"
      sed \
        -e 's|"${UMBREL_ROOT}/scripts/app"|'"${CURRENT_SCRIPT_PATH}"'|g' \
        -e 's|"${UMBREL_ROOT}/db/user.json"|"${UMBREL_ROOT}/umbrel.yaml"|g' \
        "${EXPORTS_APP_FILE}" > "${exports_app_temp_file}"
      . "${exports_app_temp_file}"
      rm -f "${exports_app_temp_file}"
    fi
  done

  # App specific exports
  # Re-assert these after sourcing dependency exports so an app cannot
  # accidentally override the runtime-owned path contract.
  export APP_DATA_DIR="${UMBREL_ROOT}/app-data/${app}"
  export APP_DATA_ROOT="${_umbrel_runtime_app_data_root}"
  export APP_ID="${app}"
  export APP_MANIFEST_FILE="${APP_DATA_DIR}/umbrel-app.yml"
  export APP_VERSION=$(cat "${APP_MANIFEST_FILE}" | yq '.version')
  
  # The manifest port remains the browser-facing port owned by umbreld's app gateway.
  export APP_PROXY_PORT=$(cat "${APP_MANIFEST_FILE}" | yq '.port')
  
  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:${GATEWAY_IP}:${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

# Hooks such as pre-install, pre-stop, pre-update, and pre-uninstall run before
# source_app in their command paths, so establish the app data contract here.
_umbrel_runtime_set_app_paths "${app}"

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.
  if [[ "${command}" == "images" ]]; then
    # Keep exports' diagnostic output out of the machine-readable image list.
    source_app "${app}" >&2
  else
    source_app "${app}"
  fi

  # Define support compose files
  local -r app_gateway_compose_file="${SCRIPT_DOCKER_FRAGMENTS}/docker-compose.app_gateway.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 user_settings_compose_file="${app_data_dir}/docker-compose.umbrel-user-settings.yml"
  local app_runtime_compose_file="${app_compose_file}"

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

  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
    if [[ "${command}" == "images" ]]; then
      # Resolve the installed config without rewriting the running app's gateway
      # or runtime files. Keep the temporary file beside the original so relative
      # Compose paths retain their meaning.
      app_runtime_compose_file="$(mktemp "${app_data_dir}/docker-compose.images.XXXXXX")"
      _umbrel_runtime_track_temp_file "${app_runtime_compose_file}"
      yq 'del(.services.app_proxy)' "${app_compose_file}" > "${app_runtime_compose_file}"
    else
      # Render app_proxy configuration with the exact environment Docker Compose
      # uses for the app. Umbreld consumes this file for its in-process gateway;
      # the app_proxy service itself is still removed from the runtime compose.
      local app_gateway_config_file="${app_data_dir}/app-gateway.json"
      local app_gateway_config_temp_file="${app_gateway_config_file}.tmp.$$"
      if ! docker compose \
        --project-name "${app}" \
        --file "${app_gateway_compose_file}" \
        --file "${app_compose_file}" \
        config --format json \
        | jq '(.services.app_proxy.environment // {}) | {
            APP_HOST: .APP_HOST,
            APP_PORT: .APP_PORT,
            APP_PROTOCOL: .APP_PROTOCOL,
            PROXY_AUTH_ADD: .PROXY_AUTH_ADD,
            PROXY_AUTH_WHITELIST: .PROXY_AUTH_WHITELIST,
            PROXY_AUTH_BLACKLIST: .PROXY_AUTH_BLACKLIST,
            PROXY_TRUST_UPSTREAM: .PROXY_TRUST_UPSTREAM,
            PROXY_TIMEOUT: .PROXY_TIMEOUT
          } | with_entries(select(.value != null))' > "${app_gateway_config_temp_file}"
      then
        rm -f "${app_gateway_config_temp_file}"
        return 1
      fi
      mv "${app_gateway_config_temp_file}" "${app_gateway_config_file}"
      chmod 600 "${app_gateway_config_file}"

      # app_proxy is now implemented in umbreld. Keep its declaration in the
      # installed compose file as gateway configuration, but never create a sidecar.
      app_runtime_compose_file="${app_data_dir}/docker-compose.umbreld.yml"
      yq 'del(.services.app_proxy)' "${app_compose_file}" > "${app_runtime_compose_file}"
    fi
  elif [[ "${command}" != "images" ]]; then
    rm -f "${app_data_dir}/app-gateway.json"
  fi

  # If remote Tor access is enabled and the app has an app_proxy service
  # then include a compose file for Tor. The hidden service points at the
  # host app gateway, so apps without one (e.g. host network apps
  # like Tailscale) must not get a tor_server sidecar, it would crash loop
  # trying to resolve a hostname that never exists.
  if [[ "${REMOTE_TOR_ACCESS}" == "true" ]] && [[ "${has_app_proxy_service}" == "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_runtime_compose_file}" )
  if [[ -f "${user_settings_compose_file}" ]]; then
    compose_files+=( "--file" "${user_settings_compose_file}" )
  fi

  # 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}" ]]; then
        # Copy to a temporary file and rename it into place so a dependent
        # sourcing this app's exports.sh (or umbreld reading its compose file
        # and manifest) only ever sees the complete old or new file, never the
        # truncated file an in-place copy leaves while it writes.
        local app_file_destination="${app_data_dir}/$(basename "${app_file}")"
        local app_file_temp_file
        app_file_temp_file="$(mktemp "${app_file_destination}.tmp.XXXXXX")"
        _umbrel_runtime_track_temp_file "${app_file_temp_file}"
        if $cp --archive "${app_file}" "${app_file_temp_file}" && mv -f "${app_file_temp_file}" "${app_file_destination}"; then
          continue
        else
          local copy_status=$?
          # Clean up failed copies even when already running inside an EXIT trap.
          rm -f "${app_file_temp_file}" || true
          return "${copy_status}"
        fi
      elif [[ -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}..."
    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
}

# Recreates install-time state after the user abandons a missing data root.
# Existing images are retained, so recovery does not require another download.
if [[ "$command" = "initialize-data-root" ]]; then
  execute_hook "${app}" "pre-install"

  start_app "${app}"

  execute_hook "${app}" "post-install"
  exit
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 containers for app ${app}..."
  compose "${app}" down --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

# Removes app containers without running hooks. This is reserved for recovery
# when storage is unavailable, and for the final teardown during uninstall.
# Image removal belongs to umbreld's sweep, which protects stopped apps too.
if [[ "$command" = "force-stop" ]]; then
  compose "${app}" down --remove-orphans
  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
  if [[ -n "${3:-}" ]]; then
    compose "${app}" logs --tail 500 | tail --bytes "${3}"
  else
    compose "${app}" logs --tail 500
  fi

  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}"

  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"

  # Keep the previous manifest if exports, templates, or image pulls fail.
  UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"

  copy_app_files "${UPDATE_FILES_WHITELIST_PRE}"

  # 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

  # Publish the new manifest after package preparation succeeds.
  copy_app_files "${UPDATE_FILES_WHITELIST_POST}"

  if [[ "$*" != *"--skip-start"* ]]; then
    "${0}" "start" "${app}"
  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}"

  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"

  # Keep the previous manifest if loading exports or rendering templates fails.
  # Umbreld needs the new metadata once this file phase succeeds.
  UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"

  copy_app_files "${UPDATE_FILES_WHITELIST_PRE}"

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

  # Now apply templates
  template_app "${app}"

  # Publish the new manifest for umbreld's compose patching and startup phases.
  copy_app_files "${UPDATE_FILES_WHITELIST_POST}"

  exit
fi

# Update an installed app
if [[ "$command" = "post-patch-update" ]]; then
  # umbreld pulls and starts the app between the split update phases. Keeping
  # startup in TypeScript lets normal starts and fresh-data initialization use
  # the same recovery path.
  execute_hook "${app}" "post-update"

  exit
fi

if [[ "$command" = "images" ]]; then
  compose "${app}" --profile '*' config --images
  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
