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

readonly APP_NAME="campaign-quartermaster"
readonly DEFAULT_VERSION="v1.2.2"
readonly DEFAULT_INSTALL_DIR="/opt/$APP_NAME"
readonly DEFAULT_BACKUP_DIR="/var/backups/$APP_NAME"
readonly DEFAULT_PORT="8484"
readonly DEFAULT_MEMORY_LIMIT="1g"
readonly DEFAULT_CPU_LIMIT="1.0"
readonly RELEASE_BASE_URL="https://github.com/Mptower/campaign-quartermaster/releases/download"

INSTALL_DIR="${CQ_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
VERSION="${CQ_VERSION:-$DEFAULT_VERSION}"
COMPOSE_FILE=""
ENV_FILE=""
TEMP_DIR=""
BACKUP_DIR="${CQ_BACKUP_DIR:-$DEFAULT_BACKUP_DIR}"
UPDATE_BACKUP=""
ROLLBACK_READY=0
ROLLING_BACK=0

fail() {
  printf 'ERROR: %s\n' "$*" >&2
  rollback_update
  exit 1
}

on_error() {
  local status=$?
  local line="${BASH_LINENO[0]:-${LINENO}}"
  trap - ERR
  printf 'ERROR: Docker installation failed at line %s (exit %s).\n' "$line" "$status" >&2
  rollback_update
  printf 'Review: docker compose --env-file %q -f %q logs --tail=100\n' \
    "${ENV_FILE:-$INSTALL_DIR/.env}" "${COMPOSE_FILE:-$INSTALL_DIR/compose.yaml}" >&2
  exit "$status"
}

cleanup() {
  if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" ]]; then
    rm -rf -- "$TEMP_DIR"
  fi
}

trap on_error ERR
trap cleanup EXIT

validate_version() {
  [[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
}

validate_port() {
  [[ "$1" =~ ^[0-9]{1,5}$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 ))
}

validate_memory_limit() {
  local value="${1,,}" amount unit factor
  [[ "$value" =~ ^([0-9]+(\.[0-9]+)?)(b|k|kb|kib|m|mb|mib|g|gb|gib|t|tb|tib)?$ ]] ||
    return 1
  amount="${BASH_REMATCH[1]}"
  unit="${BASH_REMATCH[3]:-b}"
  case "$unit" in
    b) factor=1 ;;
    k | kb | kib) factor=1024 ;;
    m | mb | mib) factor=1048576 ;;
    g | gb | gib) factor=1073741824 ;;
    t | tb | tib) factor=1099511627776 ;;
    *) return 1 ;;
  esac
  awk -v amount="$amount" -v factor="$factor" \
    'BEGIN { bytes = amount * factor; exit !(bytes >= 1073741824 && bytes <= 1099511627776) }'
}

validate_cpu_limit() {
  local value="$1" whole fraction=""
  [[ "$value" =~ ^([0-9]{1,3})(\.([0-9]+))?$ ]] || return 1
  whole="${BASH_REMATCH[1]}"
  fraction="${BASH_REMATCH[3]:-}"
  ((10#$whole >= 1 && 10#$whole <= 128)) || return 1
  [[ "$whole" != "128" || -z "$fraction" || "$fraction" =~ ^0+$ ]]
}

validate_bind_address() {
  local address="$1" octet
  [[ "$address" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]] ||
    return 1
  for octet in \
    "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" \
    "${BASH_REMATCH[3]}" "${BASH_REMATCH[4]}"; do
    ((10#$octet <= 255)) || return 1
  done
}

validate_hostname() {
  local name="$1" label
  [[ -n "$name" ]] && ((${#name} <= 253)) || return 1
  label='[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
  [[ "$name" =~ ^${label}(\.${label})*$ ]]
}

canonicalize_ipv4() {
  local address="$1" first second third fourth
  validate_bind_address "$address" || return 1
  IFS=. read -r first second third fourth <<<"$address"
  local octet
  for octet in "$first" "$second" "$third" "$fourth"; do
    [[ "$octet" == "0" || "$octet" != 0* ]] || return 1
  done
  printf '%d.%d.%d.%d' \
    "$((10#$first))" "$((10#$second))" "$((10#$third))" "$((10#$fourth))"
}

validate_ipv6_literal() {
  local address="$1" left right section segment
  local count=0 compressed=0
  local -a segments=()
  [[ -n "$address" && "$address" =~ ^[0-9a-fA-F:]+$ ]] || return 1
  if [[ "$address" == *::* ]]; then
    compressed=1
    left="${address%%::*}"
    right="${address#*::}"
    [[ "$right" != *::* ]] || return 1
  else
    [[ "$address" != :* && "$address" != *: ]] || return 1
    left="$address"
    right=""
  fi
  for section in "$left" "$right"; do
    [[ -n "$section" ]] || continue
    IFS=: read -r -a segments <<<"$section"
    for segment in "${segments[@]}"; do
      [[ "$segment" =~ ^[0-9a-fA-F]{1,4}$ ]] || return 1
      count=$((count + 1))
    done
  done
  if ((compressed)); then
    ((count < 8))
  else
    ((count == 8))
  fi
}

canonicalize_public_origin() {
  local origin="$1" host port="" canonical_host
  [[ "$origin" =~ ^[Hh][Tt][Tt][Pp][Ss]://([A-Za-z0-9.-]+|\[[0-9a-fA-F:]+\])(:([0-9]+))?/?$ ]] ||
    return 1
  host="${BASH_REMATCH[1]}"
  port="${BASH_REMATCH[3]:-}"
  if [[ "$host" == \[*\] ]]; then
    validate_ipv6_literal "${host:1:${#host}-2}" || return 1
  elif [[ "$host" =~ ^[0-9.]+$ ]]; then
    host="$(canonicalize_ipv4 "$host")" || return 1
  else
    validate_hostname "$host" || return 1
  fi
  if [[ -n "$port" ]]; then
    validate_port "$port" || return 1
    port="$((10#$port))"
  fi
  canonical_host="${host,,}"
  if [[ -z "$port" || "$port" == "443" ]]; then
    printf 'https://%s' "$canonical_host"
  else
    printf 'https://%s:%s' "$canonical_host" "$port"
  fi
}

validate_origin() {
  local origin="$1"
  if [[ "$origin" =~ ^https://([A-Za-z0-9.-]+|\[[0-9a-fA-F:]+\])(:([0-9]+))?$ ]]; then
    [[ -z "${BASH_REMATCH[3]:-}" ]] || validate_port "${BASH_REMATCH[3]}"
    return
  fi
  if [[ "$origin" =~ ^http://(localhost|127\.0\.0\.1|\[::1\])(:([0-9]+))?$ ]]; then
    [[ -z "${BASH_REMATCH[3]:-}" ]] || validate_port "${BASH_REMATCH[3]}"
    return
  fi
  return 1
}

validate_bind_origin() {
  local bind_address="$1" app_origin="$2"
  [[ "$app_origin" != http://* || "$bind_address" == "127.0.0.1" ]]
}

validate_deployment_settings() {
  local bind_address="$1" port="$2" app_origin="$3"
  local memory_limit="$4" cpu_limit="$5"
  validate_bind_address "$bind_address" || fail "CQ_BIND_ADDRESS is invalid."
  validate_port "$port" || fail "CQ_PORT must be an integer from 1 through 65535."
  validate_origin "$app_origin" ||
    fail "APP_ORIGIN must be exact HTTPS or localhost HTTP without a path."
  validate_bind_origin "$bind_address" "$app_origin" ||
    fail "HTTP origins may bind only to 127.0.0.1."
  validate_memory_limit "$memory_limit" ||
    fail "CQ_MEMORY_LIMIT must be at least 1 GiB and no more than 1 TiB."
  validate_cpu_limit "$cpu_limit" ||
    fail "CQ_CPU_LIMIT must be from 1.0 through 128."
}

prompt() {
  local variable_name="$1"
  local message="$2"
  local default_value="$3"
  local value=""

  if [[ "${CQ_NONINTERACTIVE:-0}" == "1" ]]; then
    printf -v "$variable_name" '%s' "$default_value"
    return
  fi
  [[ -r /dev/tty ]] ||
    fail "Interactive input is unavailable. Set CQ_NONINTERACTIVE=1 and the CQ_* options."
  read -r -p "$message [$default_value]: " value </dev/tty
  printf -v "$variable_name" '%s' "${value:-$default_value}"
}

prompt_public_origin() {
  local variable_name="$1" value="" canonical=""
  while true; do
    [[ -r /dev/tty ]] ||
      fail "Interactive input is unavailable. Set CQ_NONINTERACTIVE=1 and APP_ORIGIN explicitly."
    read -r -p "Public HTTPS URL for a reverse proxy (optional; press Enter for local access): " value </dev/tty
    if [[ -z "$value" ]]; then
      printf -v "$variable_name" ''
      return
    fi
    if canonical="$(canonicalize_public_origin "$value")"; then
      printf -v "$variable_name" '%s' "$canonical"
      return
    fi
    printf '%s\n' \
      "Invalid public URL. Enter an exact HTTPS origin such as https://quartermaster.example.org:8443, with no credentials, path, query, or fragment." >/dev/tty
  done
}

configure_new_install() {
  local mode="${CQ_INSTALL_MODE:-}"
  local bind_address="${CQ_BIND_ADDRESS:-127.0.0.1}"
  local port="${CQ_PORT:-$DEFAULT_PORT}"
  local app_origin="${APP_ORIGIN:-}"
  local memory_limit="${CQ_MEMORY_LIMIT:-$DEFAULT_MEMORY_LIMIT}"
  local cpu_limit="${CQ_CPU_LIMIT:-$DEFAULT_CPU_LIMIT}"

  if [[ -z "$mode" ]]; then
    if [[ "${CQ_NONINTERACTIVE:-0}" == "1" ]]; then
      if [[ -n "${CQ_BIND_ADDRESS+x}${CQ_PORT+x}${APP_ORIGIN+x}" ||
        -n "${CQ_MEMORY_LIMIT+x}${CQ_CPU_LIMIT+x}" ]]; then
        mode="advanced"
      else
        mode="quick"
      fi
    else
      prompt mode "Installation mode (quick or advanced)" "quick"
    fi
  fi
  [[ "$mode" == "quick" || "$mode" == "advanced" ]] ||
    fail "CQ_INSTALL_MODE must be quick or advanced."

  if [[ "$mode" == "quick" ]]; then
    bind_address="127.0.0.1"
    port="$DEFAULT_PORT"
    if [[ -z "$app_origin" ]]; then
      if [[ "${CQ_NONINTERACTIVE:-0}" == "1" ]]; then
        app_origin="http://localhost:$DEFAULT_PORT"
      else
        prompt_public_origin app_origin
        [[ -n "$app_origin" ]] || app_origin="http://localhost:$DEFAULT_PORT"
      fi
    elif [[ "$app_origin" == [Hh][Tt][Tt][Pp][Ss]://* ]]; then
      app_origin="$(canonicalize_public_origin "$app_origin")" ||
        fail "APP_ORIGIN must be exact HTTPS with no credentials, path, query, or fragment."
    elif [[ "$app_origin" != "http://localhost:$DEFAULT_PORT" &&
      "$app_origin" != "http://127.0.0.1:$DEFAULT_PORT" ]]; then
      fail "Quick mode APP_ORIGIN must be public HTTPS or match the fixed local endpoint http://localhost:$DEFAULT_PORT."
    fi
    memory_limit="$DEFAULT_MEMORY_LIMIT"
    cpu_limit="$DEFAULT_CPU_LIMIT"
    printf 'Quick install selected: using 127.0.0.1:%s, %s RAM, and %s CPU.\n' \
      "$DEFAULT_PORT" "$DEFAULT_MEMORY_LIMIT" "$DEFAULT_CPU_LIMIT"
  else
    prompt bind_address "Host bind address" "$bind_address"
    prompt port "Host port" "$port"
    if [[ -z "$app_origin" ]]; then
      if [[ "$bind_address" == "127.0.0.1" ]]; then
        app_origin="http://localhost:$port"
      else
        fail "Set APP_ORIGIN to the exact HTTPS origin before binding beyond loopback."
      fi
    fi
    prompt app_origin "Exact browser origin" "$app_origin"
    prompt memory_limit "Memory limit (minimum 1g)" "$memory_limit"
    prompt cpu_limit "CPU limit (minimum 1.0)" "$cpu_limit"
  fi
  if [[ "$app_origin" == [Hh][Tt][Tt][Pp][Ss]://* ]]; then
    app_origin="$(canonicalize_public_origin "$app_origin")" ||
      fail "APP_ORIGIN must be exact HTTPS with no credentials, path, query, or fragment."
  fi

  NEW_BIND_ADDRESS="$bind_address"
  NEW_PORT="$port"
  NEW_APP_ORIGIN="$app_origin"
  NEW_MEMORY_LIMIT="$memory_limit"
  NEW_CPU_LIMIT="$cpu_limit"
}

print_access_guidance() {
  local app_origin="$1" bind_address="$2" port="$3"
  if [[ "$app_origin" == https://* ]]; then
    printf '\nNginx Proxy Manager:\n'
    if [[ "$bind_address" == "0.0.0.0" ]]; then
      printf '  Host-network proxy: scheme http | private host IP (not 0.0.0.0) | port %s\n' "$port"
    else
      printf '  Host-network proxy: scheme http | Forward Hostname/IP: %s | Forward Port: %s\n' \
        "$bind_address" "$port"
    fi
    printf '  Preserve the external Host header; select the public certificate and enable Force SSL.\n'
    if [[ "$bind_address" == "127.0.0.1" ]]; then
      printf '  The proxy must share this host network namespace. Containerized NPM cannot reach the host via 127.0.0.1;\n'
      printf '  attach both services to a private shared Docker network and proxy to app:3000 instead of exposing 0.0.0.0.\n'
    fi
    printf '  Direct localhost or host-IP browser access is rejected by design while APP_ORIGIN is %s.\n' "$app_origin"
  fi
}

compose() {
  docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
}

backup_existing_state() {
  local container_id=""
  local backup_name=""
  local database_script=""
  backup_name="pre-update-$(date -u +%Y%m%dT%H%M%SZ).db"

  container_id="$(compose ps -q app)"
  [[ -n "$container_id" ]] ||
    fail "The existing deployment is not running; refusing an update without a database backup."
  install -d -m 0700 -o root -g root "$BACKUP_DIR"
  database_script=$'import Database from "better-sqlite3";\n'
  database_script+=$'const source = new Database("/data/campaign.db");\n'
  database_script+=$'await source.backup(`/data/${process.env.BACKUP_NAME}`);\n'
  database_script+=$'source.close();\n'
  database_script+=$'const backup = new Database(`/data/${process.env.BACKUP_NAME}`, { readonly: true });\n'
  database_script+=$'const check = backup.pragma("quick_check", { simple: true });\n'
  database_script+=$'backup.close();\n'
  database_script+=$'if (check !== "ok") process.exit(1);\n'
  compose exec -T \
    --env "BACKUP_NAME=$backup_name" \
    app node --input-type=module --eval "$database_script"
  compose cp "app:/data/$backup_name" "$BACKUP_DIR/$backup_name"
  chmod 0600 "$BACKUP_DIR/$backup_name"
  compose exec -T \
    --env "BACKUP_NAME=$backup_name" \
    app node --input-type=module --eval \
    'import { unlinkSync } from "node:fs"; unlinkSync(`/data/${process.env.BACKUP_NAME}`)'
  UPDATE_BACKUP="$BACKUP_DIR/$backup_name"
}

download_verified_compose() {
  local target="$1"
  local checksum_target="$2"
  local url="$RELEASE_BASE_URL/$VERSION/compose.yaml"

  if [[ -n "${CQ_COMPOSE_FILE:-}" ]]; then
    [[ -f "$CQ_COMPOSE_FILE" ]] || fail "CQ_COMPOSE_FILE does not exist: $CQ_COMPOSE_FILE"
    cp -- "$CQ_COMPOSE_FILE" "$target"
    sha256sum "$target" | awk '{print $1 "  compose.yaml"}' >"$checksum_target"
    return
  fi

  curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 --silent --show-error \
    --output "$target" "$url"
  curl --fail --location --proto '=https' --proto-redir '=https' --tlsv1.2 --silent --show-error \
    --output "$checksum_target" "$url.sha256"
  (
    cd "$(dirname "$target")"
    sha256sum --check --strict "$(basename "$checksum_target")"
  )
}

parse_setup_result() {
  local result="${1%$'\r'}"
  case "$result" in
    $'created\t'*)
      local secret="${result#*$'\t'}"
      [[ "$secret" =~ ^[A-Za-z0-9_-]{43}$ ]] || return 1
      SETUP_STATUS="created"
      SETUP_SECRET="$secret"
      ;;
    pending|complete)
      SETUP_STATUS="$result"
      SETUP_SECRET=""
      ;;
    *)
      return 1
      ;;
  esac
}

wait_for_health() {
  local container_id=""
  local status=""
  local _attempt
  for _attempt in $(seq 1 60); do
    container_id="$(compose ps -q app)"
    if [[ -n "$container_id" ]]; then
      status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$container_id")"
      case "$status" in
        healthy) return 0 ;;
        unhealthy) return 1 ;;
      esac
    fi
    sleep 2
  done
  return 1
}

rollback_update() {
  ((ROLLBACK_READY == 1 && ROLLING_BACK == 0)) || return 0
  ROLLING_BACK=1
  set +e
  trap - ERR
  printf 'Restoring the prior Docker deployment and verified database backup...\n' >&2
  compose stop app >/dev/null 2>&1 || true
  if ! install -m 0644 -o root -g root \
      "$INSTALL_DIR/compose.yaml.previous" "$COMPOSE_FILE" ||
    ! install -m 0640 -o root -g root \
      "$INSTALL_DIR/env.previous" "$ENV_FILE"; then
    printf 'Automatic rollback could not restore the prior deployment files.\n' >&2
    ROLLBACK_READY=0
    return 1
  fi
  if [[ -n "$UPDATE_BACKUP" && -f "$UPDATE_BACKUP" ]]; then
    # The root-owned backup directory remains 0700 while the file is briefly
    # readable by the image's unprivileged node user through this exact bind.
    chmod 0644 "$UPDATE_BACKUP"
    local restore_status=0
    compose run --rm --no-deps \
        --volume "$UPDATE_BACKUP:/rollback.db:ro" \
        --entrypoint sh app -c \
        'rm -f -- /data/campaign.db /data/campaign.db-wal /data/campaign.db-shm &&
         install -m 0600 /rollback.db /data/campaign.db' \
        >/dev/null || restore_status=$?
    chmod 0600 "$UPDATE_BACKUP"
    if ((restore_status != 0)); then
      printf 'Automatic rollback could not restore the verified database backup.\n' >&2
      ROLLBACK_READY=0
      return 1
    fi
  fi
  if ! compose up --detach --no-build --remove-orphans >/dev/null; then
    printf 'The prior deployment files and database were restored, but the container could not start.\n' >&2
    ROLLBACK_READY=0
    return 1
  fi
  if wait_for_health; then
    printf 'The prior container and database were restored and are healthy.\n' >&2
  else
    printf 'Rollback completed, but the prior container did not become healthy.\n' >&2
  fi
  ROLLBACK_READY=0
}

read_env_value() {
  local key="$1"
  sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1
}

main() {
  [[ "$EUID" -eq 0 ]] || fail "Run this installer as root, for example with sudo."
  [[ "$(uname -s)" == "Linux" ]] || fail "Docker installation is supported on Linux hosts."
  validate_version "$VERSION" || fail "CQ_VERSION must be a release such as v1.2.2."

  local command
  for command in docker curl sha256sum awk sed install; do
    command -v "$command" >/dev/null 2>&1 || fail "Required command not found: $command"
  done
  docker info >/dev/null 2>&1 || fail "The Docker daemon is not available to root."
  docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is required."

  if [[ -e "$INSTALL_DIR" && ! -d "$INSTALL_DIR" ]]; then
    fail "$INSTALL_DIR exists but is not a directory."
  fi
  if [[ -d "$INSTALL_DIR" ]]; then
    if [[ -f "$INSTALL_DIR/compose.yaml" || -f "$INSTALL_DIR/.env" ]]; then
      [[ -f "$INSTALL_DIR/compose.yaml" && -f "$INSTALL_DIR/.env" ]] ||
        fail "$INSTALL_DIR contains an incomplete deployment; refusing to overwrite it."
    else
      shopt -s nullglob dotglob
      local existing_entries=("$INSTALL_DIR"/*)
      shopt -u nullglob dotglob
      (( ${#existing_entries[@]} == 0 )) ||
        fail "$INSTALL_DIR is not an installer-managed Docker deployment."
    fi
  fi
  install -d -m 0755 -o root -g root "$INSTALL_DIR"
  COMPOSE_FILE="$INSTALL_DIR/compose.yaml"
  ENV_FILE="$INSTALL_DIR/.env"
  TEMP_DIR="$(mktemp -d "$INSTALL_DIR/.install.XXXXXX")"

  local bind_address="${CQ_BIND_ADDRESS:-127.0.0.1}"
  local port="${CQ_PORT:-$DEFAULT_PORT}"
  local app_origin="${APP_ORIGIN:-}"
  local memory_limit="${CQ_MEMORY_LIMIT:-$DEFAULT_MEMORY_LIMIT}"
  local cpu_limit="${CQ_CPU_LIMIT:-$DEFAULT_CPU_LIMIT}"
  local image_version="${VERSION#v}"

  if [[ -f "$ENV_FILE" ]]; then
    bind_address="$(read_env_value CQ_BIND_ADDRESS)"
    port="$(read_env_value CQ_PORT)"
    app_origin="$(read_env_value APP_ORIGIN)"
    memory_limit="$(read_env_value CQ_MEMORY_LIMIT)"
    cpu_limit="$(read_env_value CQ_CPU_LIMIT)"
    [[ -n "$bind_address" && -n "$port" && -n "$app_origin" &&
      -n "$memory_limit" && -n "$cpu_limit" ]] ||
      fail "$ENV_FILE is incomplete; refusing to replace an existing deployment configuration."
    validate_deployment_settings \
      "$bind_address" "$port" "$app_origin" "$memory_limit" "$cpu_limit"
    cp -- "$COMPOSE_FILE" "$INSTALL_DIR/compose.yaml.previous"
    cp -- "$ENV_FILE" "$INSTALL_DIR/env.previous"
    chmod 0644 "$INSTALL_DIR/compose.yaml.previous"
    chmod 0640 "$INSTALL_DIR/env.previous"
    ROLLBACK_READY=1
    printf 'Preserving the existing bind address, port, origin, and named volume.\n'
    printf 'Creating and validating the required pre-update database backup...\n'
    backup_existing_state
    compose stop app
  else
    local NEW_BIND_ADDRESS="" NEW_PORT="" NEW_APP_ORIGIN=""
    local NEW_MEMORY_LIMIT="" NEW_CPU_LIMIT=""
    configure_new_install
    bind_address="$NEW_BIND_ADDRESS"
    port="$NEW_PORT"
    app_origin="$NEW_APP_ORIGIN"
    memory_limit="$NEW_MEMORY_LIMIT"
    cpu_limit="$NEW_CPU_LIMIT"
    validate_deployment_settings \
      "$bind_address" "$port" "$app_origin" "$memory_limit" "$cpu_limit"
  fi

  download_verified_compose "$TEMP_DIR/compose.yaml" "$TEMP_DIR/compose.yaml.sha256"

  install -m 0644 -o root -g root "$TEMP_DIR/compose.yaml" "$COMPOSE_FILE"

  umask 0027
  cat >"$TEMP_DIR/env" <<EOF
CQ_VERSION=$image_version
CQ_BIND_ADDRESS=$bind_address
CQ_PORT=$port
APP_ORIGIN=$app_origin
CQ_MEMORY_LIMIT=$memory_limit
CQ_CPU_LIMIT=$cpu_limit
LOG_LEVEL=${LOG_LEVEL:-info}
EOF
  install -m 0640 -o root -g root "$TEMP_DIR/env" "$ENV_FILE"

  printf 'Starting Campaign Quartermaster %s...\n' "$VERSION"
  if [[ "${CQ_SKIP_PULL:-0}" != "1" ]]; then
    compose pull
  fi
  if [[ -n "${CQ_COMPOSE_FILE:-}" ]]; then
    compose up --detach --build --remove-orphans
  else
    compose up --detach --no-build --remove-orphans
  fi
  if ! wait_for_health; then
    compose logs --tail=100 >&2 || true
    fail "The application did not become healthy."
  fi

  local setup_output=""
  setup_output="$(compose exec -T app node dist-server/setup-secret.js --ensure --machine)"
  local SETUP_STATUS=""
  local SETUP_SECRET=""
  parse_setup_result "$setup_output" ||
    fail "The setup utility returned an unexpected response; no credential was changed."

  ROLLBACK_READY=0
  printf '\nCampaign Quartermaster is healthy.\n'
  printf 'URL: %s/\n' "${app_origin%/}"
  print_access_guidance "$app_origin" "$bind_address" "$port"
  printf 'Management directory: %s\n' "$INSTALL_DIR"
  if [[ -n "$UPDATE_BACKUP" ]]; then
    printf 'Pre-update database backup: %s\n' "$UPDATE_BACKUP"
  fi
  if [[ "$SETUP_STATUS" == "created" ]]; then
    printf '\nOne-time Commander setup secret (shown once):\n%s\n' "$SETUP_SECRET"
    printf 'Open the URL now, claim the secret, and choose the Commander password.\n'
  elif [[ "$SETUP_STATUS" == "pending" ]]; then
    printf 'Initial setup is still pending. The existing secret was not replaced or revealed.\n'
    printf 'Use the original secret; if it was lost, follow the documented pre-setup regeneration procedure.\n'
  else
    printf 'Commander setup is already complete; credentials and state were not changed.\n'
  fi
  printf 'Logs: sudo docker compose --env-file %s -f %s logs -f\n' "$ENV_FILE" "$COMPOSE_FILE"
}

if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi
