#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ENV_FILE="$ROOT_DIR/.env.appliance"
ENV_EXAMPLE="$ROOT_DIR/.env.appliance.example"
COMPOSE_FILE="$ROOT_DIR/compose.yaml"
DEVELOPMENT_COMPOSE_FILE="$ROOT_DIR/compose.development.yaml"
SECRETS_DIR="$ROOT_DIR/.uim-secrets"
BACKUP_DIR="$ROOT_DIR/backups"

usage() {
  printf '%s\n' \
    'Usage: ./uimctl <command>' \
    '' \
    '  doctor       Check Docker, Compose and local configuration' \
    '  init         Generate local configuration and appliance secrets' \
    '  install      Pull images, initialize the database and start the appliance' \
    '  build        Build the appliance image from this source tree' \
    '  dev-install  Build from source and start the appliance' \
    '  start        Start the appliance' \
    '  stop         Stop containers while preserving data' \
    '  restart      Restart the appliance' \
    '  status       Show container and health status' \
    '  logs         Follow simulator logs' \
    '  open         Open the simulator in the default browser' \
    '  backup       Create a logical MySQL backup' \
    '  restore      Restore a backup after explicit confirmation' \
    '  upgrade      Back up, pull the configured images and restart' \
    '  help         Show this message'
}

die() {
  printf 'Error: %s\n' "$*" >&2
  exit 1
}

compose() {
  [[ -f "$ENV_FILE" ]] || die 'Run ./uimctl init first.'
  docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
}

compose_development() {
  [[ -f "$ENV_FILE" ]] || die 'Run ./uimctl init first.'
  docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f "$DEVELOPMENT_COMPOSE_FILE" "$@"
}

env_value() {
  local key="$1"
  local fallback="$2"
  local value
  value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)"
  printf '%s' "${value:-$fallback}"
}

configure_paths() {
  SECRETS_DIR="$(env_value UIM_SECRETS_DIR ./.uim-secrets)"
  case "$SECRETS_DIR" in /*) ;; *) SECRETS_DIR="$ROOT_DIR/$SECRETS_DIR" ;; esac
}

verify_package() {
  if [[ ! -f "$ROOT_DIR/RELEASE.json" ]]; then
    [[ -f "$ROOT_DIR/package.json" ]] && return 0
    die 'RELEASE.json is missing from the customer package.'
  fi
  [[ -f "$ROOT_DIR/SHA256SUMS" ]] || die 'Release checksums are missing.'
  if command -v sha256sum >/dev/null 2>&1; then
    (cd "$ROOT_DIR" && sha256sum --check SHA256SUMS) || die 'Package checksum verification failed.'
  else
    (cd "$ROOT_DIR" && shasum -a 256 --check SHA256SUMS) || die 'Package checksum verification failed.'
  fi
}

prepare_images() {
  verify_package
  if [[ -f "$ROOT_DIR/images.tar" ]]; then
    docker image load --input "$ROOT_DIR/images.tar"
    # Offline means no registry calls, including during upgrades.
    compose config --images | while IFS= read -r image; do
      docker image inspect "$image" >/dev/null || die "Offline image missing: $image"
    done
  else
    compose pull
  fi
}

start_appliance() {
  # Explicitly rerun migration even when the previous one-shot container exited 0.
  compose up -d --pull never --wait mysql
  compose stop simulator
  compose run --rm --no-deps --pull never migrate
  compose up -d --no-deps --pull never simulator
  wait_ready
}

random_hex() {
  local bytes="$1"
  if command -v openssl >/dev/null 2>&1; then
    openssl rand -hex "$bytes"
  else
    od -An -N "$bytes" -tx1 /dev/urandom | tr -d ' \n'
  fi
}

write_secret() {
  local name="$1"
  local bytes="$2"
  local path="$SECRETS_DIR/$name"
  if [[ ! -s "$path" ]]; then
    [[ ! -e "$path" ]] || chmod 600 "$path"
    random_hex "$bytes" >"$path"
    chmod 444 "$path"
    return 0
  fi
  return 0
}

init_appliance() {
  [[ -f "$ENV_EXAMPLE" ]] || die '.env.appliance.example is missing.'
  if [[ ! -f "$ENV_FILE" ]]; then
    cp "$ENV_EXAMPLE" "$ENV_FILE"
    chmod 600 "$ENV_FILE"
    printf 'Created %s\n' "$ENV_FILE"
  fi

  configure_paths
  mkdir -p "$SECRETS_DIR" "$BACKUP_DIR"
  chmod 700 "$SECRETS_DIR" "$BACKUP_DIR"
  write_secret mysql_password 32
  write_secret mysql_root_password 32
  write_secret session_secret 48

  local admin_created=false
  if [[ ! -s "$SECRETS_DIR/admin_password" ]]; then admin_created=true; fi
  write_secret admin_password 18
  write_secret viewer_password 18
  # File-backed Compose secrets preserve host ownership/mode on Linux.
  # The containing directory stays 0700; container users can read mounted files.
  chmod 444 "$SECRETS_DIR"/{mysql_password,mysql_root_password,session_secret,admin_password,viewer_password}

  printf 'Appliance configuration is initialized.\n'
  if [[ "$admin_created" == true ]]; then
    printf 'Initial admin username: %s\n' "$(env_value UIM_ADMIN_USER admin)"
    printf 'Initial admin password: %s\n' "$(<"$SECRETS_DIR/admin_password")"
    printf 'Store this password securely; it remains available in %s.\n' "$SECRETS_DIR/admin_password"
  fi
}

doctor() {
  command -v docker >/dev/null 2>&1 || die 'Docker is not installed.'
  docker compose version >/dev/null 2>&1 || die 'Docker Compose v2 is not installed.'
  docker info >/dev/null 2>&1 || die 'The Docker daemon is not running or is not accessible.'
  [[ -f "$ENV_FILE" ]] || die 'Appliance configuration is missing; run ./uimctl init.'
  configure_paths
  for secret in mysql_password mysql_root_password session_secret admin_password viewer_password; do
    [[ -s "$SECRETS_DIR/$secret" ]] || die "Secret $secret is missing; run ./uimctl init."
  done
  compose config --quiet
  printf 'Docker, Compose, configuration and secrets are ready.\n'
}

wait_ready() {
  local attempt
  for attempt in {1..90}; do
    if compose exec -T simulator node -e "fetch('http://127.0.0.1:3035/api/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" >/dev/null 2>&1; then
      printf 'UIM Simulator is ready at http://%s:%s\n' \
        "$(env_value UIM_BIND_ADDRESS 127.0.0.1)" \
        "$(env_value UIM_PORT 3035)"
      return 0
    fi
    sleep 2
  done
  compose ps
  compose logs --tail=100 simulator migrate mysql
  die 'The appliance did not become ready within three minutes.'
}

install_appliance() {
  verify_package
  init_appliance
  doctor
  prepare_images
  start_appliance
}

build_appliance() {
  init_appliance
  doctor
  compose_development build simulator
  printf 'Built image %s.\n' "$(env_value UIM_SIMULATOR_IMAGE onurnizam/uim-simulator:0.1.1)"
}

backup_appliance() {
  doctor
  mkdir -p "$BACKUP_DIR"
  local target="${1:-$BACKUP_DIR/uim-simulator-$(date -u +%Y%m%dT%H%M%SZ)-$RANDOM.sql}"
  [[ ! -e "$target" && ! -e "$target.partial" ]] || die "Backup path already exists: $target"
  compose exec -T mysql sh -c 'MYSQL_PWD="$(cat /run/secrets/mysql_root_password)" exec mysqldump --single-transaction --routines --triggers --add-drop-database --databases -uroot "$MYSQL_DATABASE"' >"$target.partial"
  [[ -s "$target.partial" ]] || die 'Backup is empty.'
  mv "$target.partial" "$target"
  chmod 600 "$target"
  printf 'Backup created: %s\n' "$target"
}

restore_appliance() {
  local source="${1:-}"
  [[ -n "$source" ]] || die 'Usage: ./uimctl restore <backup.sql>'
  [[ -f "$source" ]] || die "Backup not found: $source"
  doctor
  printf 'This replaces the current simulator database. Type RESTORE to continue: '
  local confirmation
  read -r confirmation
  [[ "$confirmation" == 'RESTORE' ]] || die 'Restore cancelled.'
  compose stop simulator
  backup_appliance
  compose exec -T mysql sh -c 'MYSQL_PWD="$(cat /run/secrets/mysql_root_password)" exec mysql -uroot "$MYSQL_DATABASE"' <"$source"
  start_appliance
}

upgrade_appliance() {
  doctor
  prepare_images
  compose stop simulator
  backup_appliance
  start_appliance
}

open_appliance() {
  [[ -f "$ENV_FILE" ]] || die 'Run ./uimctl init first.'
  local url="http://$(env_value UIM_BIND_ADDRESS 127.0.0.1):$(env_value UIM_PORT 3035)"
  case "$(uname -s)" in
    Darwin) open "$url" ;;
    Linux) command -v xdg-open >/dev/null 2>&1 && xdg-open "$url" || printf '%s\n' "$url" ;;
    *) printf '%s\n' "$url" ;;
  esac
}

# Fail closed: an interrupted upgrade/restore leaves the application stopped.
trap 'printf "%s\n" "Command failed. Check ./uimctl status and migration logs. If maintenance started, keep the app stopped until recovery is complete." >&2' ERR
command="${1:-help}"
shift || true
case "$command" in
  doctor) doctor ;;
  init) init_appliance ;;
  install) install_appliance ;;
  build) build_appliance ;;
  dev-install) build_appliance; start_appliance ;;
  start) doctor; start_appliance ;;
  stop) compose down ;;
  restart) doctor; start_appliance ;;
  status) doctor; compose ps ;;
  logs) compose logs -f --tail=200 simulator ;;
  open) open_appliance ;;
  backup) backup_appliance "$@" ;;
  restore) restore_appliance "$@" ;;
  upgrade) upgrade_appliance ;;
  help|-h|--help) usage ;;
  *) usage; die "Unknown command: $command" ;;
esac
