#!/usr/bin/env bash
# Campaign Quartermaster - Proxmox VE container installer.
#
# Run this on a Proxmox VE node as root. It provisions a hardened, unprivileged
# Debian 12 LXC container and performs a native (non-Docker) installation of
# Campaign Quartermaster inside it.
#
# Primary UX executes the checksum-verified release file with terminal stdin.
#
# Non-interactive UX (validated environment variables):
#   CQ_NONINTERACTIVE=1 CQ_STORAGE=local-lvm CQ_TEMPLATE_STORAGE=local \
#     CQ_BRIDGE=vmbr0 CQ_NET_MODE=dhcp bash scripts/proxmox-install.sh
#
# This script never prints, logs, or persists the one-time Commander setup
# secret except in the final success block when it is freshly created.

set -Eeuo pipefail

# --- locate and load the shared library ------------------------------------
# When this script is piped from stdin (curl -fsSL ... | bash) BASH_SOURCE[0]
# is empty, so it cannot be found next to the script. Resolution order:
#   1. CQ_LIB_PATH  - explicit local file
#   2. alongside the script (normal checkout / release tarball)
#   3. system install path
#   4. checksum-verified helper from the same versioned GitHub release
_cq_source_common() {
  if [[ -n "${CQ_LIB_PATH:-}" && -f "${CQ_LIB_PATH}" ]]; then
    # shellcheck disable=SC1090
    source "${CQ_LIB_PATH}"
    return 0
  fi
  local self="${BASH_SOURCE[0]:-}" dir=""
  if [[ -n "$self" ]]; then
    dir="$(cd -- "$(dirname -- "$self")" 2>/dev/null && pwd || true)"
    if [[ -n "$dir" && -f "$dir/lib/proxmox-common.sh" ]]; then
      # shellcheck source=scripts/lib/proxmox-common.sh
      source "$dir/lib/proxmox-common.sh"
      return 0
    fi
  fi
  if [[ -f "/usr/local/lib/campaign-quartermaster/proxmox-common.sh" ]]; then
    # shellcheck disable=SC1091
    source "/usr/local/lib/campaign-quartermaster/proxmox-common.sh"
    return 0
  fi
  local release="${CQ_VERSION:-v1.2.2}"
  [[ "$release" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
  [[ "$release" == v* ]] || release="v$release"
  local lib_url="${CQ_LIB_URL:-https://github.com/Mptower/campaign-quartermaster/releases/download/${release}/proxmox-common.sh}"
  local checksum_url="${CQ_LIB_SHA256_URL:-${lib_url}.sha256}"
  command -v curl >/dev/null 2>&1 || return 1
  command -v sha256sum >/dev/null 2>&1 || return 1
  local tmp
  tmp="$(mktemp -d)" || return 1
  if ! curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 \
      -o "$tmp/proxmox-common.sh" "$lib_url" ||
    ! curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 \
      -o "$tmp/proxmox-common.sh.sha256" "$checksum_url" ||
    ! (
      cd "$tmp"
      sha256sum --check --strict proxmox-common.sh.sha256
    ) >/dev/null; then
    rm -rf -- "$tmp"
    return 1
  fi
  # shellcheck disable=SC1090
  source "$tmp/proxmox-common.sh"
  rm -rf -- "$tmp"
}
if ! _cq_source_common; then
  printf 'ERROR: unable to load a checksum-verified proxmox-common.sh from the local checkout or versioned release.\n' >&2
  exit 1
fi

cq_install_err_trap

# --- cleanup / rollback state ----------------------------------------------
CREATED_CT=0            # 1 once pct create succeeds for our CTID
CREATED_CTID=""         # the CTID we created (only ours)
TEMPLATE_DOWNLOADED=0   # 1 if this run downloaded the template
TEMPLATE_VOLID=""       # volid of the template we use
INSTALL_SUCCEEDED=0     # 1 only after the final healthcheck passes

cleanup() {
  local status=$?
  trap - ERR EXIT
  if ((INSTALL_SUCCEEDED)); then
    exit "$status"
  fi
  # Only destroy resources created by THIS failed run. Never touch preexisting
  # containers or storage.
  if ((CREATED_CT)) && [[ -n "$CREATED_CTID" ]]; then
    cq_warn "Install failed; cleaning up the container this run created (CTID ${CREATED_CTID})."
    pct stop "$CREATED_CTID" >/dev/null 2>&1 || true
    # protection is only set on success, so an in-progress CT is destroyable.
    if pct destroy "$CREATED_CTID" --purge >/dev/null 2>&1; then
      cq_warn "Destroyed partially provisioned container ${CREATED_CTID}."
    else
      cq_warn "Could not automatically destroy container ${CREATED_CTID}; inspect it manually."
    fi
  else
    cq_warn "Install failed before any container was created; nothing to clean up."
  fi
  if ((TEMPLATE_DOWNLOADED)); then
    # Official templates are shared and safe to keep; never delete on failure.
    cq_warn "Left the downloaded Debian 12 template in place (${TEMPLATE_VOLID}); it is shared and safe."
  fi
  exit "$status"
}
trap cleanup EXIT

# ---------------------------------------------------------------------------
# Preflight: refuse anything that is not a real Proxmox VE node.
# ---------------------------------------------------------------------------
require_proxmox() {
  [[ "${EUID:-$(id -u)}" -eq 0 ]] || cq_die "The Proxmox installer must run as root on the PVE node."

  # An explicit escape hatch used by CI/tests is refused loudly.
  if [[ "${CQ_ASSUME_PROXMOX:-}" == "test-invalid" ]]; then
    cq_die "Refusing to run: CQ_ASSUME_PROXMOX=test-invalid indicates a non-Proxmox test environment."
  fi

  local cmd
  for cmd in pveversion pct pvesh pveam pvesm ip curl getent sha256sum; do
    command -v "$cmd" >/dev/null 2>&1 ||
      cq_die "Required Proxmox command '${cmd}' not found. This does not look like a Proxmox VE node."
  done

  pveversion >/dev/null 2>&1 || cq_die "pveversion failed; refusing to continue on a non-Proxmox host."
  [[ -r /etc/pve/.version || -d /etc/pve ]] ||
    cq_die "/etc/pve is not present; this is not a Proxmox VE node."

  cq_log "Proxmox VE node detected: $(pveversion 2>/dev/null | head -n1)"
}

check_connectivity() {
  cq_log "Checking DNS resolution and internet access..."
  local ok=0
  if command -v getent >/dev/null 2>&1; then
    getent hosts nodejs.org >/dev/null 2>&1 && ok=1
  fi
  if ((ok == 0)); then
    if getent hosts github.com >/dev/null 2>&1; then ok=1; fi
  fi
  ((ok == 1)) || cq_die "DNS resolution failed for nodejs.org/github.com; check the node's network."
  if command -v curl >/dev/null 2>&1; then
    curl -fsS --max-time 10 -o /dev/null https://deb.debian.org/ 2>/dev/null ||
      curl -fsS --max-time 10 -o /dev/null https://nodejs.org/dist/ 2>/dev/null ||
      cq_die "No outbound HTTPS connectivity detected; the container install requires internet access."
  fi
  cq_log "Connectivity looks good."
}

# ---------------------------------------------------------------------------
# Discovery helpers (host side).
# ---------------------------------------------------------------------------
list_storage_with_content() {
  # $1: content type (rootdir|vztmpl). Print eligible enabled storage ids.
  local content="$1"
  pvesm status -content "$content" 2>/dev/null | awk 'NR>1 {print $1}'
}

storage_supports() {
  # $1: storage id, $2: content type
  local id="$1" content="$2"
  list_storage_with_content "$content" | grep -Fxq -- "$id"
}

list_bridges() {
  ip -o link show type bridge 2>/dev/null | awk -F': ' '{print $2}' | sed 's/@.*//'
}

ctid_exists() {
  local id="$1"
  pct status "$id" >/dev/null 2>&1
}

next_ctid() {
  local id
  id="$(pvesh get /cluster/nextid 2>/dev/null || true)"
  if [[ "$id" =~ ^[0-9]+$ ]]; then
    printf '%s' "$id"
    return 0
  fi
  # Fallback: scan upward from 100.
  id=100
  while ctid_exists "$id"; do id=$((id + 1)); done
  printf '%s' "$id"
}

# ---------------------------------------------------------------------------
# Prompting (interactive) - reads from /dev/tty so curl-pipe works.
# ---------------------------------------------------------------------------
HAS_TTY=0
if [[ -r /dev/tty ]]; then HAS_TTY=1; fi
HAS_WHIPTAIL=0
if command -v whiptail >/dev/null 2>&1; then HAS_WHIPTAIL=1; fi
WIZARD_ENABLED=0

has_explicit_install_options() {
  [[ -n "${CQ_INSTALL_MODE+x}${CQ_CTID+x}${CQ_HOSTNAME+x}${CQ_STORAGE+x}" ||
    -n "${CQ_TEMPLATE_STORAGE+x}${CQ_BRIDGE+x}${CQ_NET_MODE+x}${CQ_CIDR+x}" ||
    -n "${CQ_GATEWAY+x}${CQ_VLAN+x}${CQ_DISK_GB+x}${CQ_CORES+x}${CQ_RAM_MB+x}" ||
    -n "${CQ_SWAP_MB+x}${CQ_PORT+x}${CQ_TIMEZONE+x}${CQ_TEMPLATE+x}" ||
    -n "${CQ_APP_ORIGIN+x}${CQ_TRUST_PROXY+x}" ]]
}

terminal_is_suitable() {
  [[ -t 0 && -t 1 && -n "${TERM:-}" && "${TERM:-}" != "dumb" ]]
}

can_use_wizard() {
  ((HAS_WHIPTAIL == 1 && HAS_TTY == 1)) || return 1
  terminal_is_suitable || return 1
  ! has_explicit_install_options
}

wizard_cancel() {
  trap - ERR EXIT
  INSTALL_SUCCEEDED=1
  printf '\nCampaign Quartermaster installation cancelled before provisioning; no changes were made.\n'
  exit 0
}

wizard_command_failed() {
  local screen="$1" status="$2"
  cq_die "The whiptail ${screen} screen failed (exit ${status}); no provisioning has started."
}

wizard_welcome() {
  ((WIZARD_ENABLED)) || return 0
  whiptail --title "Campaign Quartermaster" \
    --yes-button "Continue" --no-button "Cancel" --yesno \
    "Guided Proxmox VE installation\n\nCreates one unprivileged Debian 12 LXC. Docker, nesting, host device passthrough, and arbitrary Host/Origin access remain disabled.\n\nQuick mode uses 1 core, 1024 MiB RAM, 1024 MiB swap, 8 GiB disk, DHCP, and HTTPS port 8484.\n\nContinue?" \
    18 76 || {
      local status=$?
      ((status == 1 || status == 255)) && wizard_cancel
      wizard_command_failed "welcome" "$status"
    }
}

wizard_status() {
  local message="$1"
  ((WIZARD_ENABLED)) || return 0
  whiptail --title "Campaign Quartermaster - Installing" --infobox \
    "$message\n\nProvisioning has started. Do not close this terminal; failures trigger the existing cleanup procedure." \
    10 76 || {
      local status=$?
      cq_warn "Could not update the whiptail status screen (exit ${status}); continuing with terminal logs."
      WIZARD_ENABLED=0
    }
}

read_tty_value() {
  local __var="$1" value=""
  IFS= read -r value </dev/tty || return 1
  printf -v "$__var" '%s' "$value"
}

ask() {
  # ask <result_var> <prompt> <default> <validator_fn>
  local __var="$1" prompt="$2" default="$3" validator="$4" answer=""
  while true; do
    if ((WIZARD_ENABLED)); then
      answer="$(whiptail --title "Campaign Quartermaster" \
        --inputbox "$prompt" 10 72 "$default" 3>&1 1>&2 2>&3)" || {
          local status=$?
          ((status == 1 || status == 255)) && wizard_cancel
          wizard_command_failed "input" "$status"
        }
    else
      if [[ -n "$default" ]]; then
        printf '%s [%s]: ' "$prompt" "$default" >/dev/tty
      else
        printf '%s: ' "$prompt" >/dev/tty
      fi
      read_tty_value answer || answer=""
    fi
    [[ -z "$answer" ]] && answer="$default"
    if "$validator" "$answer"; then
      printf -v "$__var" '%s' "$answer"
      return 0
    fi
    if ((WIZARD_ENABLED)); then
      whiptail --title "Invalid value" --msgbox \
        "The value is not valid for ${prompt}.\n\nReview the requested format and try again." \
        10 72 || {
          local status=$?
          ((status == 1 || status == 255)) && wizard_cancel
          wizard_command_failed "validation" "$status"
        }
    else
      printf 'Invalid value: %s\n' "$answer" >/dev/tty
    fi
  done
}

ask_choice() {
  # ask_choice <result_var> <prompt> <default> <option...>
  local __var="$1" prompt="$2" default="$3"
  shift 3
  local options=("$@") answer=""
  while true; do
    if ((WIZARD_ENABLED)); then
      local menu=() opt initial="OFF"
      for opt in "${options[@]}"; do
        [[ "$opt" == "$default" ]] && initial="ON" || initial="OFF"
        case "$opt" in
          quick) menu+=("$opt" "Stable defaults; only an optional public URL follows" "$initial") ;;
          advanced) menu+=("$opt" "Customize storage, network, and supported resources" "$initial") ;;
          dhcp) menu+=("$opt" "Obtain the guest address automatically" "$initial") ;;
          static) menu+=("$opt" "Set an address, prefix, and gateway" "$initial") ;;
          yes) menu+=("$opt" "Create the reviewed container" "$initial") ;;
          no) menu+=("$opt" "Cancel without making changes" "$initial") ;;
          *) menu+=("$opt" "$opt" "$initial") ;;
        esac
      done
      answer="$(whiptail --title "Campaign Quartermaster" --radiolist \
        "$prompt" 15 78 6 "${menu[@]}" 3>&1 1>&2 2>&3)" || {
          local status=$?
          ((status == 1 || status == 255)) && wizard_cancel
          wizard_command_failed "choice" "$status"
        }
    else
      printf '%s (options: %s) [%s]: ' "$prompt" "${options[*]}" "$default" >/dev/tty
      read_tty_value answer || answer=""
    fi
    [[ -z "$answer" ]] && answer="$default"
    local opt
    for opt in "${options[@]}"; do
      if [[ "$answer" == "$opt" ]]; then
        printf -v "$__var" '%s' "$answer"
        return 0
      fi
    done
    printf 'Choose one of: %s\n' "${options[*]}" >/dev/tty
  done
}

ask_public_origin() {
  local __var="$1" answer="" canonical=""
  local prompt="Public HTTPS URL for a reverse proxy (optional; leave blank for direct guest-IP access)"
  while true; do
    if ((WIZARD_ENABLED)); then
      answer="$(whiptail --title "Campaign Quartermaster" \
        --inputbox "$prompt" 10 78 "" 3>&1 1>&2 2>&3)" || {
          local status=$?
          ((status == 1 || status == 255)) && wizard_cancel
          wizard_command_failed "public URL" "$status"
        }
    else
      printf '%s: ' "$prompt" >/dev/tty
      read_tty_value answer || answer=""
    fi
    if [[ -z "$answer" ]]; then
      printf -v "$__var" ''
      return 0
    fi
    if canonical="$(cq_canonicalize_public_origin "$answer")"; then
      printf -v "$__var" '%s' "$canonical"
      return 0
    fi
    local invalid_message="Enter an exact HTTPS origin such as https://quartermaster.example.org:8443, with no credentials, path, query, or fragment."
    if ((WIZARD_ENABLED)); then
      whiptail --title "Invalid public HTTPS URL" --msgbox "$invalid_message" 10 78 || {
        local status=$?
        ((status == 1 || status == 255)) && wizard_cancel
        wizard_command_failed "URL validation" "$status"
      }
    else
      printf 'Invalid public URL. %s\n' "$invalid_message" >/dev/tty
    fi
  done
}

configuration_summary() {
  local origin="${OPT_APP_ORIGIN:-Direct guest-IP HTTPS (assigned after DHCP)}"
  local network="$OPT_NET_MODE"
  if [[ "$OPT_NET_MODE" == "static" ]]; then
    network="${OPT_CIDR}, gateway ${OPT_GATEWAY}"
  fi
  printf 'Mode: %s\nContainer: %s (%s)\nStorage: %s; template storage: %s\nNetwork: %s on %s' \
    "$OPT_INSTALL_MODE" "$OPT_CTID" "$OPT_HOSTNAME" "$OPT_STORAGE" \
    "$OPT_TMPL_STORAGE" "$network" "$OPT_BRIDGE"
  [[ -z "$OPT_VLAN" ]] || printf '; VLAN %s' "$OPT_VLAN"
  printf '\nResources: %s core(s), %s MiB RAM, %s MiB swap, %s GiB disk\nHTTPS port: %s\nBrowser origin: %s' \
    "$OPT_CORES" "$OPT_RAM_MB" "$OPT_SWAP_MB" "$OPT_DISK_GB" "$OPT_PORT" "$origin"
}

confirm_configuration() {
  local summary
  summary="$(configuration_summary)"
  if ((WIZARD_ENABLED)); then
    whiptail --title "Review configuration" \
      --yes-button "Install" --no-button "Cancel" --yesno \
      "${summary}\n\nCreate this container and begin provisioning?" 22 82 || {
        local status=$?
        ((status == 1 || status == 255)) && wizard_cancel
        wizard_command_failed "review" "$status"
      }
  else
    printf '\nConfiguration review:\n%s\n\n' "$summary" >/dev/tty
    local proceed=""
    while true; do
      printf 'Begin provisioning? (options: yes no) [yes]: ' >/dev/tty
      read_tty_value proceed || wizard_cancel
      proceed="${proceed:-yes}"
      case "$proceed" in
        yes) return 0 ;;
        no) wizard_cancel ;;
        *) printf 'Choose one of: yes no\n' >/dev/tty ;;
      esac
    done
  fi
}

validate_nonempty() { [[ -n "${1-}" ]]; }

# ---------------------------------------------------------------------------
# Option gathering.
# ---------------------------------------------------------------------------
gather_options() {
  local noninteractive=0
  cq_is_truthy "${CQ_NONINTERACTIVE:-0}" && noninteractive=1
  if ((noninteractive == 0 && HAS_TTY == 0)); then
    cq_die "No interactive terminal is available. Set CQ_NONINTERACTIVE=1 and review every CQ_* option."
  fi
  if ((noninteractive == 0)) && can_use_wizard; then
    WIZARD_ENABLED=1
    wizard_welcome
  else
    WIZARD_ENABLED=0
  fi

  local default_ctid
  default_ctid="$(next_ctid)"

  local default_storage default_tmpl_storage
  default_storage="$(list_storage_with_content rootdir | head -n1)"
  default_tmpl_storage="$(list_storage_with_content vztmpl | head -n1)"
  local default_bridge
  default_bridge="$(list_bridges | head -n1)"
  [[ -n "$default_bridge" ]] || default_bridge="vmbr0"

  local install_mode="${CQ_INSTALL_MODE:-}"
  if [[ -z "$install_mode" ]]; then
    if ((noninteractive)); then
      if [[ -n "${CQ_CTID+x}${CQ_HOSTNAME+x}${CQ_STORAGE+x}${CQ_TEMPLATE_STORAGE+x}" ||
        -n "${CQ_BRIDGE+x}${CQ_NET_MODE+x}${CQ_CIDR+x}${CQ_GATEWAY+x}${CQ_VLAN+x}" ||
        -n "${CQ_DISK_GB+x}${CQ_CORES+x}${CQ_RAM_MB+x}${CQ_SWAP_MB+x}${CQ_PORT+x}" ||
        -n "${CQ_TIMEZONE+x}${CQ_TEMPLATE+x}${CQ_APP_ORIGIN+x}${CQ_TRUST_PROXY+x}" ]]; then
        install_mode="advanced"
      else
        install_mode="quick"
      fi
    else
      ask_choice install_mode "Installation mode" "quick" quick advanced
    fi
  fi
  [[ "$install_mode" == "quick" || "$install_mode" == "advanced" ]] ||
    cq_die "CQ_INSTALL_MODE must be quick or advanced."
  OPT_INSTALL_MODE="$install_mode"

  if [[ "$install_mode" == "quick" ]]; then
    OPT_CTID="$default_ctid"
    OPT_HOSTNAME="campaign-quartermaster"
    OPT_STORAGE="$default_storage"
    OPT_TMPL_STORAGE="$default_tmpl_storage"
    OPT_BRIDGE="$default_bridge"
    OPT_NET_MODE="dhcp"
    OPT_CIDR=""
    OPT_GATEWAY=""
    OPT_VLAN=""
    OPT_DISK_GB="8"
    OPT_CORES="1"
    OPT_RAM_MB="1024"
    OPT_SWAP_MB="1024"
    OPT_PORT="$CQ_DEFAULT_PORT"
    OPT_TIMEZONE="UTC"
    OPT_TEMPLATE=""
    OPT_APP_ORIGIN="${CQ_APP_ORIGIN:-}"
    if ((noninteractive == 0)) && [[ -z "$OPT_APP_ORIGIN" ]]; then
      ask_public_origin OPT_APP_ORIGIN
    fi
    cq_log "Quick install selected: DHCP guest with HTTPS on port ${CQ_DEFAULT_PORT}."
  elif ((noninteractive)); then
    OPT_CTID="${CQ_CTID:-$default_ctid}"
    OPT_HOSTNAME="${CQ_HOSTNAME:-campaign-quartermaster}"
    OPT_STORAGE="${CQ_STORAGE:-$default_storage}"
    OPT_TMPL_STORAGE="${CQ_TEMPLATE_STORAGE:-$default_tmpl_storage}"
    OPT_BRIDGE="${CQ_BRIDGE:-$default_bridge}"
    OPT_NET_MODE="${CQ_NET_MODE:-dhcp}"
    OPT_CIDR="${CQ_CIDR:-}"
    OPT_GATEWAY="${CQ_GATEWAY:-}"
    OPT_VLAN="${CQ_VLAN:-}"
    OPT_DISK_GB="${CQ_DISK_GB:-8}"
    OPT_CORES="${CQ_CORES:-1}"
    OPT_RAM_MB="${CQ_RAM_MB:-1024}"
    OPT_SWAP_MB="${CQ_SWAP_MB:-1024}"
    OPT_PORT="${CQ_PORT:-$CQ_DEFAULT_PORT}"
    OPT_TIMEZONE="${CQ_TIMEZONE:-UTC}"
    OPT_TEMPLATE="${CQ_TEMPLATE:-}"
    OPT_APP_ORIGIN="${CQ_APP_ORIGIN:-}"
  else
    cq_log "Advanced setup selected. Press Enter to accept the shown default."
    ask OPT_CTID "Container ID (CTID)" "${CQ_CTID:-$default_ctid}" cq_validate_ctid
    ask OPT_HOSTNAME "Container hostname" "${CQ_HOSTNAME:-campaign-quartermaster}" cq_validate_hostname
    ask OPT_STORAGE "Storage for the container rootfs (rootdir-capable)" "${CQ_STORAGE:-$default_storage}" cq_validate_storage_name
    ask OPT_TMPL_STORAGE "Storage for the Debian 12 template (vztmpl-capable)" "${CQ_TEMPLATE_STORAGE:-$default_tmpl_storage}" cq_validate_storage_name
    ask OPT_BRIDGE "Network bridge" "${CQ_BRIDGE:-$default_bridge}" cq_validate_bridge
    ask_choice OPT_NET_MODE "Network mode" "${CQ_NET_MODE:-dhcp}" dhcp static
    if [[ "$OPT_NET_MODE" == "static" ]]; then
      ask OPT_CIDR "Static address (CIDR, e.g. 192.168.1.50/24)" "${CQ_CIDR:-}" cq_validate_cidr
      ask OPT_GATEWAY "Gateway IPv4 address" "${CQ_GATEWAY:-}" cq_validate_ipv4
    else
      OPT_CIDR=""
      OPT_GATEWAY=""
    fi
    ask OPT_VLAN "VLAN tag (blank for none)" "${CQ_VLAN:-}" _validate_optional_vlan
    ask OPT_DISK_GB "Root disk size (GiB, minimum 8)" "${CQ_DISK_GB:-8}" _validate_disk
    ask OPT_CORES "CPU cores (minimum 1)" "${CQ_CORES:-1}" _validate_cores
    ask OPT_RAM_MB "Memory (MiB, minimum 1024)" "${CQ_RAM_MB:-1024}" _validate_ram
    ask OPT_SWAP_MB "Swap (MiB, minimum 1024)" "${CQ_SWAP_MB:-1024}" _validate_swap
    ask OPT_PORT "HTTPS application port" "${CQ_PORT:-$CQ_DEFAULT_PORT}" cq_validate_port
    ask OPT_TIMEZONE "Timezone" "${CQ_TIMEZONE:-UTC}" cq_validate_timezone
    OPT_TEMPLATE="${CQ_TEMPLATE:-}"
    OPT_APP_ORIGIN="${CQ_APP_ORIGIN:-}"
    if [[ -z "$OPT_APP_ORIGIN" ]]; then
      ask_public_origin OPT_APP_ORIGIN
    fi
  fi

  validate_options
  ((noninteractive)) || confirm_configuration
  cq_log "Validated ${OPT_INSTALL_MODE} install settings."
}

_validate_optional_vlan() { [[ -z "${1-}" ]] || cq_validate_vlan "$1"; }
_validate_disk() { cq_validate_int_range "${1-}" 8 4096; }
_validate_cores() { cq_validate_int_range "${1-}" 1 128; }
_validate_ram() { cq_validate_int_range "${1-}" 1024 1048576; }
_validate_swap() { cq_validate_int_range "${1-}" 1024 1048576; }

validate_options() {
  OPT_APP_ORIGIN="${OPT_APP_ORIGIN:-${CQ_APP_ORIGIN:-}}"
  cq_validate_ctid "$OPT_CTID" || cq_die "Invalid CTID: ${OPT_CTID}"
  cq_validate_hostname "$OPT_HOSTNAME" || cq_die "Invalid hostname: ${OPT_HOSTNAME}"
  cq_validate_storage_name "$OPT_STORAGE" || cq_die "Invalid rootfs storage: ${OPT_STORAGE}"
  cq_validate_storage_name "$OPT_TMPL_STORAGE" || cq_die "Invalid template storage: ${OPT_TMPL_STORAGE}"
  cq_validate_bridge "$OPT_BRIDGE" || cq_die "Invalid bridge: ${OPT_BRIDGE}"
  case "$OPT_NET_MODE" in
    dhcp) ;;
    static)
      cq_validate_cidr "$OPT_CIDR" || cq_die "Invalid static CIDR: ${OPT_CIDR}"
      cq_validate_ipv4 "$OPT_GATEWAY" || cq_die "Invalid gateway: ${OPT_GATEWAY}"
      ;;
    *) cq_die "CQ_NET_MODE must be dhcp or static." ;;
  esac
  [[ -z "$OPT_VLAN" ]] || cq_validate_vlan "$OPT_VLAN" || cq_die "Invalid VLAN tag: ${OPT_VLAN}"
  _validate_disk "$OPT_DISK_GB" || cq_die "Invalid disk size: ${OPT_DISK_GB}"
  _validate_cores "$OPT_CORES" || cq_die "Invalid core count: ${OPT_CORES}"
  _validate_ram "$OPT_RAM_MB" || cq_die "Invalid memory: ${OPT_RAM_MB}"
  _validate_swap "$OPT_SWAP_MB" || cq_die "Invalid swap: ${OPT_SWAP_MB}"
  cq_validate_port "$OPT_PORT" || cq_die "Invalid port: ${OPT_PORT}"
  cq_validate_timezone "$OPT_TIMEZONE" || cq_die "Invalid timezone: ${OPT_TIMEZONE}"
  if [[ -n "$OPT_TEMPLATE" ]]; then
    [[ "$OPT_TEMPLATE" =~ ^debian-12-standard_[A-Za-z0-9._+-]+_(amd64|arm64)\.tar\.(zst|gz|xz)$ ]] ||
      cq_die "CQ_TEMPLATE must name a Debian 12 standard template archive."
  fi

  if [[ -n "${OPT_APP_ORIGIN:-}" ]]; then
    OPT_APP_ORIGIN="$(cq_canonicalize_public_origin "$OPT_APP_ORIGIN")" ||
      cq_die "CQ_APP_ORIGIN must be an exact HTTPS origin with no credentials, path, query, or fragment."
  fi
  if [[ -n "${CQ_TRUST_PROXY:-}" ]]; then
    cq_validate_ipv4 "$CQ_TRUST_PROXY" ||
      cq_die "CQ_TRUST_PROXY must be a single specific IPv4 address; refusing 'true'/CIDR values."
  fi

  # Confirm storage capabilities against live Proxmox metadata.
  storage_supports "$OPT_STORAGE" rootdir ||
    cq_die "Storage '${OPT_STORAGE}' does not support container root filesystems (rootdir)."
  storage_supports "$OPT_TMPL_STORAGE" vztmpl ||
    cq_die "Storage '${OPT_TMPL_STORAGE}' does not support templates (vztmpl)."

  if ctid_exists "$OPT_CTID"; then
    cq_die "CTID ${OPT_CTID} already exists. Choose a different id; this installer never overwrites containers."
  fi
}

print_proxy_guidance() {
  local app_origin="$1" app_ip="$2" app_port="$3"
  printf '\nNginx Proxy Manager:\n'
  printf '  Scheme: https | Forward Hostname/IP: %s | Forward Port: %s\n' "$app_ip" "$app_port"
  printf '  Preserve the external Host header; select the public certificate and enable Force SSL.\n'
  printf '  Trust or explicitly verify the guest certificate on the proxy; restrict guest port %s to the proxy.\n' "$app_port"
  printf '  Direct guest-IP browser access is rejected by design while APP_ORIGIN is %s.\n' "$app_origin"
}

wizard_success() {
  local app_origin="$1" app_ip="$2"
  ((WIZARD_ENABLED)) || return 0
  local proxy=""
  if [[ -n "${OPT_APP_ORIGIN:-}" ]]; then
    proxy=$'\n\nNginx Proxy Manager:\nScheme: https\nForward host: '"${app_ip}"$'\nForward port: '"${OPT_PORT}"$'\nPreserve Host; choose the public certificate and enable Force SSL.'
  fi
  local setup_note="Commander setup is already complete."
  case "${CQ_SETUP_STATUS:-}" in
    created)
      setup_note="A one-time Commander setup secret will be printed once in the terminal after this window closes. It is never passed to whiptail."
      ;;
    pending)
      setup_note="A setup secret is already pending and was not changed or revealed."
      ;;
  esac
  whiptail --title "Installation complete" --infobox \
    "Campaign Quartermaster is healthy.\n\nBrowser URL: ${app_origin}/\nContainer: ${OPT_CTID}\nGuest IP: ${app_ip}${proxy}\n\n${setup_note}" \
    22 82 || cq_warn "Could not show the final whiptail summary; the complete result follows in the terminal."
}

# ---------------------------------------------------------------------------
# Template acquisition.
# ---------------------------------------------------------------------------
resolve_template() {
  cq_log "Updating template catalog (pveam update)..."
  pveam update >/dev/null 2>&1 || cq_warn "pveam update failed; using the cached catalog."

  local template="$OPT_TEMPLATE"
  if [[ -z "$template" ]]; then
    template="$(pveam available --section system 2>/dev/null |
      awk '/debian-12-standard/ {print $2}' | sort -V | tail -n1)"
  fi
  [[ -n "$template" ]] || cq_die "Could not determine a Debian 12 standard template from pveam available."
  cq_log "Selected template: ${template}"

  TEMPLATE_VOLID="${OPT_TMPL_STORAGE}:vztmpl/${template}"

  if pveam list "$OPT_TMPL_STORAGE" 2>/dev/null | awk '{print $1}' | grep -Fxq -- "$TEMPLATE_VOLID"; then
    cq_log "Template already present on ${OPT_TMPL_STORAGE}; reusing it."
    TEMPLATE_DOWNLOADED=0
  else
    cq_log "Downloading template to ${OPT_TMPL_STORAGE}..."
    pveam download "$OPT_TMPL_STORAGE" "$template" ||
      cq_die "Failed to download template ${template} to ${OPT_TMPL_STORAGE}."
    TEMPLATE_DOWNLOADED=1
  fi

  # Confirm the concrete volid resolves to a real path.
  pvesm path "$TEMPLATE_VOLID" >/dev/null 2>&1 ||
    cq_die "Template volume ${TEMPLATE_VOLID} could not be resolved via pvesm path."
}

# ---------------------------------------------------------------------------
# Container creation.
# ---------------------------------------------------------------------------
create_container() {
  export CQ_PCT_HOSTNAME="$OPT_HOSTNAME"
  export CQ_PCT_STORAGE="$OPT_STORAGE"
  export CQ_PCT_DISK_GB="$OPT_DISK_GB"
  export CQ_PCT_CORES="$OPT_CORES"
  export CQ_PCT_MEMORY_MB="$OPT_RAM_MB"
  export CQ_PCT_SWAP_MB="$OPT_SWAP_MB"
  export CQ_PCT_TIMEZONE="$OPT_TIMEZONE"

  export CQ_NET_BRIDGE="$OPT_BRIDGE"
  export CQ_NET_MODE="$OPT_NET_MODE"
  export CQ_NET_CIDR="$OPT_CIDR"
  export CQ_NET_GATEWAY="$OPT_GATEWAY"
  export CQ_NET_VLAN="$OPT_VLAN"

  local net0
  net0="$(cq_build_net0)" || cq_die "Failed to construct the network configuration."
  export CQ_PCT_NET0="$net0"

  local pct_args=()
  mapfile -t pct_args < <(cq_build_pct_args "$OPT_CTID" "$TEMPLATE_VOLID") ||
    cq_die "Failed to construct pct create arguments."

  # Re-validate collision immediately before creation to close TOCTOU windows.
  if ctid_exists "$OPT_CTID"; then
    cq_die "CTID ${OPT_CTID} appeared since validation; aborting without overwriting it."
  fi

  cq_log "Creating unprivileged Debian 12 container ${OPT_CTID}..."
  pct "${pct_args[@]}" || cq_die "pct create failed for CTID ${OPT_CTID}."
  CREATED_CT=1
  CREATED_CTID="$OPT_CTID"

  cq_log "Starting container ${OPT_CTID}..."
  pct start "$OPT_CTID" || cq_die "Failed to start container ${OPT_CTID}."
  wait_for_network
}

wait_for_network() {
  cq_log "Waiting for container networking..."
  local _attempt
  for _attempt in $(seq 1 60); do
    if pct exec "$OPT_CTID" -- getent hosts deb.debian.org >/dev/null 2>&1; then
      cq_log "Container has DNS and network."
      return 0
    fi
    sleep 2
  done
  cq_die "Container ${OPT_CTID} did not obtain working networking in time."
}

detect_ct_ip() {
  local ip=""
  if [[ "$OPT_NET_MODE" == "static" ]]; then
    ip="${OPT_CIDR%/*}"
  else
    local _attempt raw
    for _attempt in $(seq 1 30); do
      raw="$(pct exec "$OPT_CTID" -- hostname -I 2>/dev/null || true)"
      ip="$(printf '%s' "$raw" | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)"
      [[ -n "$ip" ]] && break
      sleep 2
    done
  fi
  cq_validate_ipv4 "$ip" || cq_die "Could not determine the container's IPv4 address."
  printf '%s' "$ip"
}

# ---------------------------------------------------------------------------
# Guest-side install driver.
# ---------------------------------------------------------------------------
guest_exec() {
  # Run a generated guest script (from stdin) inside the container.
  pct exec "$OPT_CTID" -- bash -s -- "$@"
}

guest_provision_script() {
  cat <<GUEST
#!/usr/bin/env bash
set -Eeuo pipefail
trap 'printf "guest ERROR: command failed (exit %s) at line %s: %s\\n" "\$?" "\$LINENO" "\$BASH_COMMAND" >&2' ERR

APP_NAME="${CQ_APP_NAME}"
APP_DIR="${CQ_APP_DIR}"
STATE_DIR="${CQ_STATE_DIR}"
CONFIG_DIR="${CQ_CONFIG_DIR}"
ENV_FILE="${CQ_ENV_FILE}"
DATABASE_PATH="${CQ_DATABASE_PATH}"
UNIT_FILE="${CQ_UNIT_FILE}"
MANAGED_MARKER="${CQ_MANAGED_MARKER}"
TLS_DIR="\${CONFIG_DIR}/tls"
TLS_CERT="\${TLS_DIR}/server.crt"
TLS_KEY="\${TLS_DIR}/server.key"
NGINX_SITE="/etc/nginx/sites-available/campaign-quartermaster"

NODE_VERSION="${CQ_NODE_VERSION}"
NODE_SHA_X64="${CQ_NODE_SHA256_X64}"
NODE_SHA_ARM64="${CQ_NODE_SHA256_ARM64}"

APP_VERSION="\$1"
APP_ORIGIN="\$2"
APP_PORT="\$3"
APP_IP="\$4"
RELEASE_URL="\$5"
RELEASE_SHA_URL="\$6"
TRUST_PROXY="\${7-}"
BACKEND_PORT=3000
if [ "\$APP_PORT" = "\$BACKEND_PORT" ]; then BACKEND_PORT=3001; fi

export DEBIAN_FRONTEND=noninteractive

echo "[guest] Installing prerequisites..."
apt-get update -y
apt-get install -y --no-install-recommends \
  ca-certificates curl xz-utils build-essential python3 git sqlite3 nginx openssl
update-ca-certificates || true

# --- Node.js (pinned, checksum verified) ---
ARCH="\$(uname -m)"
case "\$ARCH" in
  x86_64|amd64) NODE_ARCH="x64"; NODE_SHA="\$NODE_SHA_X64" ;;
  aarch64|arm64) NODE_ARCH="arm64"; NODE_SHA="\$NODE_SHA_ARM64" ;;
  *) echo "[guest] Unsupported architecture: \$ARCH" >&2; exit 1 ;;
esac
NODE_TARBALL="node-v\${NODE_VERSION}-linux-\${NODE_ARCH}.tar.xz"
NODE_URL="https://nodejs.org/dist/v\${NODE_VERSION}/\${NODE_TARBALL}"
WORK="\$(mktemp -d)"
trap 'rm -rf -- "\$WORK"' EXIT
echo "[guest] Downloading Node.js \${NODE_VERSION} (\${NODE_ARCH})..."
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "\$WORK/\$NODE_TARBALL" "\$NODE_URL"
echo "\${NODE_SHA}  \$WORK/\$NODE_TARBALL" | sha256sum -c -
install -d -m 0755 /usr/local/lib/nodejs
tar -xJf "\$WORK/\$NODE_TARBALL" -C /usr/local/lib/nodejs
NODE_HOME="/usr/local/lib/nodejs/node-v\${NODE_VERSION}-linux-\${NODE_ARCH}"
ln -sfn "\$NODE_HOME/bin/node" /usr/local/bin/node
ln -sfn "\$NODE_HOME/bin/npm" /usr/local/bin/npm
ln -sfn "\$NODE_HOME/bin/npx" /usr/local/bin/npx
export PATH="/usr/local/bin:\$PATH"
node --version
npm --version

# --- service account and directories ---
if ! getent group "\$APP_NAME" >/dev/null; then
  groupadd --system "\$APP_NAME"
fi
if ! id "\$APP_NAME" >/dev/null 2>&1; then
  useradd --system --gid "\$APP_NAME" --home-dir "\$STATE_DIR" \
    --shell /usr/sbin/nologin "\$APP_NAME"
fi
install -d -m 0755 -o root -g root "\$CONFIG_DIR"
install -d -m 0700 -o "\$APP_NAME" -g "\$APP_NAME" "\$STATE_DIR"
install -d -m 0700 -o root -g root "\$TLS_DIR"

# --- fetch and verify the release source asset ---
echo "[guest] Downloading Campaign Quartermaster \${APP_VERSION} source asset..."
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "\$WORK/src.tar.gz" "\$RELEASE_URL"
if curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "\$WORK/src.sha256" "\$RELEASE_SHA_URL"; then
  EXPECTED="\$(awk '{print \$1}' "\$WORK/src.sha256" | head -n1)"
  [[ "\$EXPECTED" =~ ^[0-9a-fA-F]{64}\$ ]] || {
    echo "[guest] Release checksum is malformed." >&2
    exit 1
  }
  echo "\${EXPECTED}  \$WORK/src.tar.gz" | sha256sum -c -
else
  echo "[guest] Release checksum sidecar not found; refusing unverifiable source." >&2
  exit 1
fi

install -d -m 0755 "\$WORK/src"
tar -xzf "\$WORK/src.tar.gz" -C "\$WORK/src" --strip-components=1

# --- build and stage a production-only tree ---
cd "\$WORK/src"
npm ci
npm run build
STAGE="\$WORK/stage"
install -d -m 0755 "\$STAGE" "\$STAGE/dist" "\$STAGE/dist-server"
install -m 0644 package.json package-lock.json "\$STAGE/"
cp -a dist/. "\$STAGE/dist/"
cp -a dist-server/. "\$STAGE/dist-server/"
( cd "\$STAGE" && npm ci --omit=dev )
[ -f "\$STAGE/dist/index.html" ] || { echo "[guest] staged browser build incomplete" >&2; exit 1; }
[ -f "\$STAGE/dist-server/index.js" ] || { echo "[guest] staged server build incomplete" >&2; exit 1; }
[ -f "\$STAGE/dist-server/setup-secret.js" ] || { echo "[guest] staged setup utility incomplete" >&2; exit 1; }
node --check "\$STAGE/dist-server/index.js"
node --check "\$STAGE/dist-server/setup-secret.js"

rm -rf -- "\$APP_DIR"
mv -- "\$STAGE" "\$APP_DIR"
chown -R root:root "\$APP_DIR"

# --- environment file ---
if [ ! -f "\$ENV_FILE" ]; then
  umask 027
  {
    echo "NODE_ENV=production"
    echo "HOST=127.0.0.1"
    echo "PORT=\${BACKEND_PORT}"
    echo "APP_ORIGIN=\${APP_ORIGIN}"
    echo "DATABASE_PATH=\${DATABASE_PATH}"
    echo "STATIC_ROOT=\${APP_DIR}/dist"
    echo "LOG_LEVEL=info"
    if [ -n "\$TRUST_PROXY" ]; then
      echo "TRUST_PROXY=\${TRUST_PROXY}"
    fi
  } >"\$ENV_FILE"
fi
chown root:"\$APP_NAME" "\$ENV_FILE"
chmod 0640 "\$ENV_FILE"

# --- unique local TLS endpoint ---
if [ ! -s "\$TLS_KEY" ] || [ ! -s "\$TLS_CERT" ]; then
  openssl req -x509 -newkey rsa:3072 -sha256 -nodes -days 825 \
    -keyout "\$TLS_KEY" -out "\$TLS_CERT" \
    -subj "/CN=\${APP_IP}" -addext "subjectAltName=IP:\${APP_IP}"
fi
chown root:root "\$TLS_KEY" "\$TLS_CERT"
chmod 0600 "\$TLS_KEY"
chmod 0644 "\$TLS_CERT"

cat >"\$NGINX_SITE" <<NGINX
server {
    listen \${APP_PORT} ssl default_server;
    server_name _;
    access_log off;

    ssl_certificate \${TLS_CERT};
    ssl_certificate_key \${TLS_KEY};
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_tickets off;

    client_max_body_size 700k;
    location / {
        proxy_pass http://127.0.0.1:\${BACKEND_PORT};
        proxy_http_version 1.1;
        proxy_set_header Host \\\$http_host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For \\\$proxy_add_x_forwarded_for;
        proxy_set_header Connection "";
    }
}
NGINX
ln -sfn "\$NGINX_SITE" /etc/nginx/sites-enabled/campaign-quartermaster
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl enable nginx.service >/dev/null

# --- hardened systemd unit ---
cat >"\$UNIT_FILE" <<UNIT
[Unit]
Description=Campaign Quartermaster
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=\${APP_NAME}
Group=\${APP_NAME}
WorkingDirectory=\${APP_DIR}
EnvironmentFile=\${ENV_FILE}
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
ExecStart=/usr/local/bin/node \${APP_DIR}/dist-server/index.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
RestrictSUIDSGID=true
RestrictNamespaces=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=true
MemoryDenyWriteExecute=false
SystemCallArchitectures=native
CapabilityBoundingSet=
AmbientCapabilities=
UMask=0077

[Install]
WantedBy=multi-user.target
UNIT
chmod 0644 "\$UNIT_FILE"

install -d -m 0755 "\$CONFIG_DIR"
printf 'managed-by=proxmox-install\nversion=%s\n' "\$APP_VERSION" >"\$MANAGED_MARKER"
chmod 0644 "\$MANAGED_MARKER"

systemctl daemon-reload
systemctl enable "\${APP_NAME}.service" >/dev/null
echo "[guest] Provisioning complete."
GUEST
}

guest_secret_script() {
  # Emits ONLY the machine status line on stdout; all logs go to stderr.
  cat <<GUEST
#!/usr/bin/env bash
set -Eeuo pipefail
APP_NAME="${CQ_APP_NAME}"
APP_DIR="${CQ_APP_DIR}"
ENV_FILE="${CQ_ENV_FILE}"
DATABASE_PATH="${CQ_DATABASE_PATH}"
export PATH="/usr/local/bin:\$PATH"
# Ensure the state dir exists for the service user before first run.
APP_ORIGIN="\$(sed -n 's/^APP_ORIGIN=//p' "\$ENV_FILE" | tail -n1)"
[ -n "\$APP_ORIGIN" ] || {
  echo "[guest] APP_ORIGIN is missing from the managed environment file." >&2
  exit 1
}
runuser -u "\$APP_NAME" -- env \
  NODE_ENV=production \
  APP_ORIGIN="\$APP_ORIGIN" \
  DATABASE_PATH="\$DATABASE_PATH" \
  STATIC_ROOT="\$APP_DIR/dist" \
  node "\$APP_DIR/dist-server/setup-secret.js" --ensure --machine
GUEST
}

guest_start_script() {
  cat <<GUEST
#!/usr/bin/env bash
set -Eeuo pipefail
APP_NAME="${CQ_APP_NAME}"
ENV_FILE="${CQ_ENV_FILE}"
TLS_CERT="${CQ_CONFIG_DIR}/tls/server.crt"
HOST_HEADER="\$1"
APP_ORIGIN="\$2"
APP_IP="\$3"
APP_PORT="\$4"
BACKEND_PORT="\$(sed -n 's/^PORT=//p' "\$ENV_FILE" | tail -n1)"
[ -n "\$BACKEND_PORT" ] || exit 1
export PATH="/usr/local/bin:\$PATH"
service_diagnostics() {
  systemctl --no-pager --full status "\${APP_NAME}.service" >&2 || true
  journalctl -u "\${APP_NAME}.service" -n 80 --no-pager \
    --output=short-precise >&2 || true
}
if ! systemctl restart "\${APP_NAME}.service"; then
  echo "[guest] service restart failed" >&2
  service_diagnostics
  exit 1
fi
systemctl restart nginx.service
ok=0
for attempt in \$(seq 1 30); do
  if curl -fsS --max-time 3 -H "Host: \${HOST_HEADER}" \
      "http://127.0.0.1:\${BACKEND_PORT}/healthz" >/dev/null 2>&1; then
    ok=1; break
  fi
  sleep 1
done
if [ "\$ok" -ne 1 ]; then
  echo "[guest] healthcheck failed" >&2
  service_diagnostics
  exit 1
fi
curl -fsS --max-time 5 --cacert "\$TLS_CERT" -H "Host: \${HOST_HEADER}" \
  "https://\${APP_IP}:\${APP_PORT}/healthz" >/dev/null
echo "[guest] service healthy"
GUEST
}

run_guest_install() {
  local app_origin="$1" host_header="$2" version="$3" app_ip="$4"
  local release_url release_sha_url
  release_url="$(cq_release_asset_url "$version")"
  release_sha_url="$(cq_release_checksum_url "$version")"

  cq_log "Provisioning the container (this downloads Node.js and builds the app)..."
  guest_provision_script | guest_exec \
    "$version" "$app_origin" "$OPT_PORT" "$app_ip" \
    "$release_url" "$release_sha_url" "${CQ_TRUST_PROXY:-}" ||
    cq_die "In-container provisioning failed."

  cq_log "Ensuring one-time setup state..."
  local setup_raw setup_status setup_secret=""
  # Capture without ever echoing; only stdout (the machine line) is captured.
  set +e
  setup_raw="$(guest_secret_script | pct exec "$OPT_CTID" -- bash -s)"
  local rc=$?
  set -e
  ((rc == 0)) || cq_die "The setup-secret step failed inside the container."

  setup_status="$(cq_setup_status "$setup_raw")" ||
    cq_die "Unrecognized setup-secret output; refusing to continue."
  case "$setup_status" in
    created)
      setup_secret="$(cq_setup_secret "$setup_raw")" ||
        cq_die "A secret was reported as created but could not be parsed."
      ;;
    pending | complete)
      : # Do not regenerate or reveal anything.
      ;;
  esac

  cq_log "Starting the service and running the health check..."
  guest_start_script | guest_exec \
    "$host_header" "$app_origin" "$app_ip" "$OPT_PORT" ||
    cq_die "The service did not become healthy."

  CQ_SETUP_STATUS="$setup_status"
  CQ_SETUP_SECRET="$setup_secret"
}

finalize_container() {
  cq_log "Applying container protection and boot settings..."
  pct set "$OPT_CTID" --onboot 1 >/dev/null ||
    cq_die "Could not set onboot=1 on container ${OPT_CTID}."
  pct set "$OPT_CTID" --protection 1 >/dev/null ||
    cq_die "Could not enable protection on container ${OPT_CTID}."
}

# ---------------------------------------------------------------------------
# Main.
# ---------------------------------------------------------------------------
main() {
  local version
  version="$(cq_resolve_version)"

  require_proxmox
  check_connectivity
  gather_options
  wizard_status "Resolving and verifying the Debian 12 template..."
  resolve_template
  wizard_status "Creating the unprivileged LXC container..."
  create_container

  local ct_ip app_origin host_header
  ct_ip="$(detect_ct_ip)"
  if [[ -n "${OPT_APP_ORIGIN:-}" ]]; then
    app_origin="$OPT_APP_ORIGIN"
  else
    app_origin="$(cq_default_app_origin "$ct_ip" "$OPT_PORT")"
  fi
  host_header="$(cq_origin_host "$app_origin")"

  wizard_status "Installing Node.js and Campaign Quartermaster inside the guest...\n\nThis build step can take several minutes."
  run_guest_install "$app_origin" "$host_header" "$version" "$ct_ip"
  local cert_fingerprint
  cert_fingerprint="$(
    pct exec "$OPT_CTID" -- \
      openssl x509 -in "$CQ_CONFIG_DIR/tls/server.crt" \
        -noout -fingerprint -sha256
  )" || cq_die "Could not read the guest TLS certificate fingerprint."
  [[ -n "$cert_fingerprint" ]] || cq_die "The guest TLS certificate fingerprint is empty."
  finalize_container

  INSTALL_SUCCEEDED=1
  trap - ERR EXIT

  wizard_success "$app_origin" "$ct_ip"
  printf '\n'
  cq_log "Campaign Quartermaster is installed and healthy."
  printf '  Container ID : %s\n' "$OPT_CTID"
  printf '  IP address   : %s\n' "$ct_ip"
  printf '  URL          : %s/\n' "$app_origin"
  printf '  Version      : %s\n' "$version"
  printf '  TLS cert     : %s\n' "$cert_fingerprint"
  if [[ -n "${OPT_APP_ORIGIN:-}" ]]; then
    print_proxy_guidance "$app_origin" "$ct_ip" "$OPT_PORT"
  fi
  printf '\n'
  printf 'Next steps:\n'
  if [[ -n "${OPT_APP_ORIGIN:-}" ]]; then
    printf '  - Restrict TCP port %s to the reverse-proxy address with the Proxmox firewall.\n' "$OPT_PORT"
    printf '  - Verify the shown guest certificate fingerprint at the reverse proxy.\n'
  else
    printf '  - Restrict TCP port %s to trusted clients with the Proxmox firewall.\n' "$OPT_PORT"
    printf '  - Verify the shown certificate fingerprint before accepting the local certificate.\n'
    printf '  - Replace the local certificate with trusted HTTPS before broader access.\n'
  fi
  printf '  - Check service: pct exec %s -- systemctl status %s --no-pager\n' \
    "$OPT_CTID" "$CQ_SERVICE_NAME"
  printf '  - Back up guest: vzdump %s --mode snapshot --storage <backup-storage>\n' "$OPT_CTID"
  printf '  - Update later: CQ_CTID=%s CQ_VERSION=vNEXT bash ./proxmox-update.sh\n' "$OPT_CTID"
  printf '\n'
  printf 'NOTE: The default certificate is unique and self-signed. It encrypts traffic\n'
  printf 'but is not publicly trusted; verify its SHA-256 fingerprint from this console.\n\n'

  case "${CQ_SETUP_STATUS:-}" in
    created)
      printf 'One-time Commander setup secret (shown once, store it now):\n\n'
      printf '    %s\n\n' "$CQ_SETUP_SECRET"
      printf 'Open %s/ and use this secret to create the Commander account now.\n' "$app_origin"
      ;;
    pending)
      printf 'A setup secret was already pending from a previous run and was NOT changed.\n'
      printf 'If you no longer have it, follow the docs to rotate it inside the container.\n'
      ;;
    complete)
      printf 'Commander bootstrap is already complete; existing credentials were preserved.\n'
      ;;
  esac
  # Scrub the secret from memory as best we can.
  unset CQ_SETUP_SECRET
}

# Run main when executed directly or piped from stdin (curl | bash), where
# BASH_SOURCE[0] is empty. When sourced (e.g. by tests) BASH_SOURCE[0] is this
# file's path and differs from $0, so main is not executed.
if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi
