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

UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
USER_FILE="${UMBREL_ROOT}/db/user.json"

show_help() {
  cat << EOF
app 0.0.1

CLI 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
    stop                       Stops an installed app
    start                      Starts an installed app
    compose                    Passes all arguments to docker-compose
    ls-installed               Lists installed apps
EOF
}

check_dependencies () {
  for cmd in "$@"; do
    if ! command -v $cmd >/dev/null 2>&1; then
      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
}

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

# Check dependencies
check_dependencies docker-compose jq openssl

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"
  app_dir="${UMBREL_ROOT}/apps/${app}"
  app_data_dir="${UMBREL_ROOT}/app-data/${app}"
  if [[ "${app}" == "installed" ]]; then
    list_installed_apps | while read app; do
      if [[ "${app}" != "" ]]; then
        "${0}" "${1}" "${app}" "${@:3}" || true
      fi
    done
    exit
  fi
  if [[ -z "${app}" ]] || [[ ! -d "${app_dir}" ]]; then
    echo "Error: \"${app}\" is not a valid app"
    exit 1
  fi
fi

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

compose() {
  local app="${1}"
  shift
  local env_file="${UMBREL_ROOT}/.env"
  local app_base_compose_file="${UMBREL_ROOT}/apps/docker-compose.common.yml"
  local app_compose_file="${app_dir}/docker-compose.yml"
  local app_hidden_servive_file="${UMBREL_ROOT}/tor/data/app-${app}/hostname"
  local app_entropy_identifier="app-${app}-seed"

  export BITCOIN_DATA_DIR="${UMBREL_ROOT}/bitcoin"
  export LND_DATA_DIR="${UMBREL_ROOT}/lnd"
  export APP_DATA_DIR="${app_data_dir}"
  export APP_HIDDEN_SERVICE="$(cat "${app_hidden_servive_file}" 2>/dev/null || echo "notyetset.onion")"
  export APP_SEED=$(derive_entropy "${app_entropy_identifier}")

  # App specific env vars
  # Note: Hardcoding app specific env vars is a short term solution. Long term
  # these values will be registered in an apps manifest and generated dynamically.
  local whirlpool_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}-whirlpool/hostname"
  export SAMOURAI_SERVER_WHIRLPOOL_HIDDEN_SERVICE="$(cat "${whirlpool_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")"
  export SAMOURAI_SERVER_NODE_API_KEY=$(derive_entropy "env-${app_entropy_identifier}-NODE_API_KEY")
  export SAMOURAI_SERVER_NODE_ADMIN_KEY=$(derive_entropy "env-${app_entropy_identifier}-NODE_ADMIN_KEY")
  export SAMOURAI_SERVER_NODE_JWT_SECRET=$(derive_entropy "env-${app_entropy_identifier}-NODE_JWT_SECRET")

  docker-compose \
    --env-file "${env_file}" \
    --project-name "${app}" \
    --file "${app_base_compose_file}" \
    --file "${app_compose_file}" \
    "${@}"
}

update_installed_apps() {
  local action="${1}"
  local app="${2}"

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

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

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

  echo "Setting up data dir for app ${app}..."
  mkdir -p "${app_data_dir}"
  rsync --archive --verbose --exclude ".gitkeep" "${app_dir}/." "${app_data_dir}"
  
  echo "Pulling images for app ${app}..."
  compose "${app}" pull

  echo "Starting app ${app}..."
  compose "${app}" up --detach

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

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

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

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

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

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

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

  exit
fi

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

  if ! list_installed_apps | grep --quiet "^${app}$"; then
    echo "Error: app \"${app}\" is not installed yet"
    exit 1
  fi

  echo "Starting app ${app}..."
  compose "${app}" up --detach

  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