#!/bin/bash
# --- ProxmoxVEx auto-header start ---
# --------------------------------------------------------------------
# File:        deploy.sh
# Project:     ProxmoxVEx
# Version:     1.2.662
# Build:       2026.10.21
# Description: Deploy SH source
# Usage:       bash deploy.sh
# Docs:        https://proxmoxvex.com/docs
# Generated:   2026-10-21
# --------------------------------------------------------------------
# --- ProxmoxVEx auto-header end ---
set -e

# =============================================================================
# Overview
# =============================================================================
# This script performs a complete installation of the ProxmoxVEx management
# platform on a Debian/Ubuntu-style host. It must be run as root. The main
# phases are:
#
#   1. Parse command-line options (--port, --no-interactive, --no-tui,
#      --no-offline).
#   2. Verify the environment (root, internet reachability).
#   3. Scan prerequisites (package manager, Python interpreter + version,
#      venv/pip modules, system tools, disk space). Whatever is missing is
#      installed step by step, prompting only where an answer can't be
#      derived — e.g. which interpreter to use. Details collected here feed
#      the application configuration (venv, systemd unit, port settings).
#   4. Pick the UI: a full-screen `dialog` frontend on real terminals (SSH
#      included — widgets read /dev/tty, so `curl | sudo bash` works too),
#      falling back to classic line output. The TUI collects every answer
#      (dependencies, port, optional components, Python 3.14 consent) up
#      front, then shows the installer's output in a live scrolling window.
#   5. Install system packages required by the application.
#   6. Create a dedicated service user and the /opt/ProxmoxVEx directory tree.
#   7. Acquire the application source (local checkout or release archive).
#   8. Build a Python virtual environment and install Python dependencies.
#   9. Optionally download static assets so the UI works offline.
#  10. Prompt for (or default to) the HTTP listening port — classic mode only;
#      the TUI has already answered this before the install started.
#  11. Create the systemd unit, sudoers rules, and helper binaries.
#  12. Bootstrap the master encryption key outside the config directory.
#  13. Start the service and persist the selected port in SQLite.
# =============================================================================

# =============================================================================
# Terminal output color definitions
# These are used by the print_* helpers below to make the installer readable.
# =============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

# =============================================================================
# Deployment configuration
# INSTALL_DIR: where the application will live on disk.
# SERVICE_USER / SERVICE_GROUP: unprivileged account used by systemd.
# RELEASE_ARCHIVE: release tarball served by the main site — the default
#                  source for curl-pipe installs (the public GitHub mirror
#                  repo only ships update artefacts, not the source tree).
# SOURCE_REPO:   optional full-source git remote (PROXMOXVEX_REPO env var) for
#                internal/dev installs that should clone instead.
# GITHUB_BRANCH: branch selector for git-clone installs. Users can override
#                 via the ProxmoxVEx_BRANCH environment variable (e.g. for
#                 testing a feature branch).
# PYTHON_FILE: the main Python entry point copied into INSTALL_DIR.
# =============================================================================
INSTALL_DIR="/opt/ProxmoxVEx"
SERVICE_USER="ProxmoxVEx"
SERVICE_GROUP="ProxmoxVEx"
RELEASE_ARCHIVE="https://proxmoxvex.com/downloads/ProxmoxVEx-latest.tar.gz"
SOURCE_REPO="${PROXMOXVEX_REPO:-}"
# MK May 2026 (#417 follow-up, elektronen): allow installing from a specific
# branch via `ProxmoxVEx_BRANCH=Testing curl ... | sudo bash`. Default still main
# so existing curl-pipe invocations don't change behaviour.
GITHUB_BRANCH="${ProxmoxVEx_BRANCH:-main}"
PYTHON_FILE="ProxmoxVEx_multi_cluster.py"

# =============================================================================
# Default install-time options
# ACCESS_PORT: default HTTPS port for the web UI.
# INTERACTIVE: whether to ask the user for port selection.
# DOWNLOAD_OFFLINE: whether to pre-fetch CDN assets before first run.
# CANARY: whether to install the canary template pack for staged rollouts.
# CANARY_REPLICATED: whether to install the canary replicated service pack.
# CANARY_SIGNED: whether to stage the canary signed image pack.
# CANARY_VULNERABILITY_SCAN: whether to stage the canary vulnerability scan pack.
# CANARY_CI: whether to stage the canary CI pipeline pack.
# CANARY_CD: whether to stage the canary CD pipeline pack.
# CANARY_UPGRADE: whether to stage the canary upgrade path pack.
# CANARY_ROLLBACK: whether to stage the canary rollback path pack.
# ROLLING_BASE: whether to stage the rolling base chart pack.
# ROLLING_MULTI_ARCH: whether to stage the rolling multi-arch build pack.
# ROLLING_TEMPLATE: whether to stage the rolling template pack.
# ROLLING_REPLICATED: whether to stage the rolling replicated service pack.
# ROLLING_SIGNED: whether to stage the rolling signed images pack.
# ROLLING_VULNERABILITY: whether to stage the rolling vulnerability scan pack.
# ROLLING_CI: whether to stage the rolling CI pipeline pack.
# ROLLING_CD: whether to stage the rolling CD pipeline pack.
# ROLLING_UPGRADE: whether to stage the rolling upgrade path pack.
# ROLLING_ROLLBACK: whether to stage the rolling rollback path pack.
# AIRGAP_BASE: whether to stage the air-gap base chart pack.
# AIRGAP_MULTI_ARCH: whether to stage the air-gap multi-arch build pack.
# HA_UPGRADE: whether to stage the HA upgrade path pack.
# HA_ROLLBACK: whether to stage the HA rollback path pack.
# =============================================================================
ACCESS_PORT=5000
INTERACTIVE=true
# PORT_VIA_ARG records whether --port pinned the port on the CLI so the TUI
# port picker can skip re-asking a question the operator already answered.
PORT_VIA_ARG=false
DOWNLOAD_OFFLINE=true
# NO_TUI (--no-tui or PROXMOXVEX_NO_TUI=1) forces the classic line output even
# on terminals where the dialog UI would normally run.
NO_TUI="${PROXMOXVEX_NO_TUI:-false}"
# PYTHON_BIN: interpreter used for the version check and to build the venv.
# The TUI dependency resolver may point this at a user-supplied path when the
# distro python is missing/out of range; PROXMOXVEX_PYTHON does the same for
# scripted installs.
PYTHON_BIN="${PROXMOXVEX_PYTHON:-python3}"
# INSTALL_CI_TOOLS: whether to install helm/trivy/hadolint for the packaging
# pipelines. On by default (historic behaviour); the TUI options checklist or
# PROXMOXVEX_CI_TOOLS=0 can disable it.
INSTALL_CI_TOOLS="${PROXMOXVEX_CI_TOOLS:-true}"
CANARY=false
CANARY_REPLICATED=false
CANARY_SIGNED=false
CANARY_VULNERABILITY_SCAN=false
CANARY_CI=false
CANARY_CD=false
CANARY_UPGRADE=false
CANARY_ROLLBACK=false
ROLLING_BASE=false
ROLLING_MULTI_ARCH=false
ROLLING_TEMPLATE=false
ROLLING_REPLICATED=false
ROLLING_SIGNED=false
ROLLING_VULNERABILITY=false
ROLLING_CI=false
ROLLING_CD=false
ROLLING_UPGRADE=false
ROLLING_ROLLBACK=false
AIRGAP_BASE=false
AIRGAP_MULTI_ARCH=false
SINGLE_NODE_BASE=false
HA_UPGRADE=false
HA_ROLLBACK=false

# =============================================================================
# Parse Arguments
# Supported flags:
#   --port=PORT          Override the default listening port.
#   --no-interactive       Skip all prompts and use ACCESS_PORT as given.
#   --no-offline           Do not run the offline asset downloader.
#   --help / -h            Show usage and exit.
# =============================================================================
for arg in "$@"; do
    case $arg in
        --port=*)
            ACCESS_PORT="${arg#*=}"
            PORT_VIA_ARG=true
            ;;
        --no-interactive)
            INTERACTIVE=false
            ;;
        --no-tui)
            NO_TUI=true
            ;;
        --no-offline)
            DOWNLOAD_OFFLINE=false
            ;;
        --canary)
            CANARY=true
            ;;
        --canary-replicated)
            CANARY_REPLICATED=true
            ;;
        --canary-signed)
            CANARY_SIGNED=true
            ;;
        --canary-vulnerability-scan)
            CANARY_VULNERABILITY_SCAN=true
            ;;
        --canary-ci)
            CANARY_CI=true
            ;;
        --canary-cd)
            CANARY_CD=true
            ;;
        --canary-upgrade)
            CANARY_UPGRADE=true
            ;;
        --canary-rollback)
            CANARY_ROLLBACK=true
            ;;
        --rolling-base-chart)
            ROLLING_BASE=true
            ;;
        --rolling-multi-arch-build)
            ROLLING_MULTI_ARCH=true
            ;;
        --rolling-template-pack)
            ROLLING_TEMPLATE=true
            ;;
        --rolling-replicated-service)
            ROLLING_REPLICATED=true
            ;;
        --rolling-signed-images)
            ROLLING_SIGNED=true
            ;;
        --rolling-vulnerability-scan)
            ROLLING_VULNERABILITY=true
            ;;
        --rolling-ci)
            ROLLING_CI=true
            ;;
        --rolling-cd)
            ROLLING_CD=true
            ;;
        --rolling-upgrade)
            ROLLING_UPGRADE=true
            ;;
        --rolling-rollback)
            ROLLING_ROLLBACK=true
            ;;
        --airgap-base-chart)
            AIRGAP_BASE=true
            ;;
        --airgap-multi-arch-build)
            AIRGAP_MULTI_ARCH=true
            ;;
        --single-node-base-chart)
            SINGLE_NODE_BASE=true
            ;;
        --ha-upgrade)
            HA_UPGRADE=true
            ;;
        --ha-rollback)
            HA_ROLLBACK=true
            ;;
        --help|-h)
            echo "ProxmoxVEx Deploy Script"
            echo ""
            echo "Usage: sudo ./deploy.sh [options]"
            echo ""
            echo "Options:"
            echo "  --port=PORT       Set web port (default: 5000, use 443 for HTTPS)"
            echo "  --no-interactive  Skip interactive prompts"
            echo "  --no-tui          Use classic line output instead of the full-screen UI"
            echo "  --no-offline      Skip offline assets download"
            echo "  --canary          Install the canary template pack for staged rollouts"
            echo "  --canary-replicated  Install the canary replicated service template pack"
            echo "  --canary-signed   Stage the canary signed image pack"
            echo "  --canary-vulnerability-scan  Stage the canary vulnerability scan pack"
            echo "  --canary-ci       Stage the canary CI pipeline pack"
            echo "  --canary-cd       Stage the canary CD pipeline pack"
            echo "  --canary-upgrade  Stage the canary upgrade path pack"
            echo "  --canary-rollback Stage the canary rollback path pack"
            echo "  --rolling-base-chart  Stage the rolling base chart pack"
            echo "  --rolling-multi-arch-build  Stage the rolling multi-arch build pack"
            echo "  --rolling-template-pack  Stage the rolling template pack"
            echo "  --rolling-replicated-service  Stage the rolling replicated service pack"
            echo "  --rolling-signed-images  Stage the rolling signed images pack"
            echo "  --rolling-vulnerability-scan  Stage the rolling vulnerability scan pack"
            echo "  --rolling-ci      Stage the rolling CI pipeline pack"
            echo "  --rolling-cd      Stage the rolling CD pipeline pack"
            echo "  --rolling-upgrade Stage the rolling upgrade path pack"
            echo "  --rolling-rollback Stage the rolling rollback path pack"
            echo "  --airgap-base-chart  Stage the air-gap base chart pack"
            echo "  --airgap-multi-arch-build  Stage the air-gap multi-arch build pack"
            echo "  --single-node-base-chart  Stage the single-node base chart pack"
            echo "  --single-node-multi-arch-build  Stage the single-node multi-arch build pack"
            echo "  --ha-upgrade      Stage the HA upgrade path pack"
            echo "  --ha-rollback     Stage the HA rollback path pack"
            echo "  --help            Show this help"
            echo ""
            echo "Environment:"
            echo "  PROXMOXVEX_PYTHON=/path/python3  Use a specific interpreter for the venv"
            echo "  PROXMOXVEX_CI_TOOLS=0            Skip helm/trivy/hadolint installation"
            echo "  PROXMOXVEX_NO_TUI=1              Force classic line output"
            echo ""
            echo "Examples:"
            echo "  sudo ./deploy.sh                     # Interactive install"
            echo "  sudo ./deploy.sh --port=443          # Use port 443"
            echo "  sudo ./deploy.sh --no-interactive    # Non-interactive with defaults"
            exit 0
            ;;
    esac
done

# =============================================================================
# Helper Functions
# Small utilities for formatted output and section banners.
# =============================================================================

# Print the large ASCII banner at the top of the install.
print_banner() {
    echo -e "${BLUE}"
    echo "╔═══════════════════════════════════════════════════════════════════════════╗"
    echo "║                                                                           ║"
    cat <<'BANNER'
║        _____                                 __      ________             ║
║       |  __ \                                \ \    / /  ____|            ║
║       | |__) | __ _____  ___ __ ___   _____  _\ \  / /| |__  __  __       ║
║       |  ___/ '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /\ \/ / |  __| \ \/ /       ║
║       | |   | | | (_) >  <| | | | | | (_) >  <  \  /  | |____ >  <        ║
║       |_|   |_|  \___/_/\_\_| |_| |_|\___/_/\_\  \/   |______/_/\_\       ║
BANNER
    echo "║                    All-in-One Deploy Script v2.0                          ║"
    echo "╚═══════════════════════════════════════════════════════════════════════════╝"
    echo -e "${NC}"
}

# Print a major section heading with horizontal rules.
print_step() {
    echo -e "\n${BLUE}═══════════════════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}$1${NC}"
    echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════${NC}\n"
}

# Colored status printers used throughout the installer.
print_success() { echo -e "${GREEN}✓${NC} $1"; }
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
print_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
print_error() { echo -e "${RED}✗${NC} $1"; }

# =============================================================================
# SSH Terminal UI (dialog)
# Sep 2026 — when the installer runs on a real terminal (SSH session or
# console), drive it through a full-screen `dialog` UI: a welcome screen, all
# questions collected up front, a live scrolling output window during the
# install, and a closing summary screen. This works for both documented
# invocations (`sudo ./deploy.sh` and `curl ... | sudo bash`) because the
# widgets read keystrokes from /dev/tty rather than stdin. Non-interactive
# runs, missing or tiny terminals, and hosts where `dialog` cannot be
# installed all fall back to the classic print_* output above.
# =============================================================================
TUI=false
TUI_LOG="/var/log/proxmoxvex-install.log"
TUI_BACKTITLE="ProxmoxVEx Installer — https://proxmoxvex.com"
TUI_PROG_H=20
TUI_PROG_W=76

# dlg: invoke a dialog widget with the shared backtitle. stdin is forced to
# /dev/tty so interactive widgets receive keystrokes even when the script
# itself was piped in via curl. The 3>&1 1>&2 2>&3 swap is the standard dialog
# capture idiom: the curses UI is drawn on stdout (→ stderr → terminal) while
# the widget's answer is emitted on stderr (→ stdout → $(...) captures it).
dlg() {
    dialog --backtitle "$TUI_BACKTITLE" "$@" 3>&1 1>&2 2>&3 </dev/tty
}

# setup_tui: decide whether the dialog UI can run on this host, installing the
# small `dialog` package if needed. Called after the root + connectivity
# checks so apt is usable, and before any screen is drawn.
setup_tui() {
    TUI=false
    [ "$INTERACTIVE" = true ] || return 0
    case "$NO_TUI" in 1|true|yes|on) return 0 ;; esac
    case "${TERM:-}" in ""|dumb|unknown) return 0 ;; esac

    # A controlling terminal is required for the widgets. This also succeeds
    # under `curl | sudo bash`: stdin is the pipe there, but /dev/tty still
    # belongs to the SSH session.
    (exec </dev/tty) 2>/dev/null || return 0

    if ! command -v dialog >/dev/null 2>&1; then
        print_info "Installing 'dialog' for the installer UI..."
        apt-get update -qq >/dev/null 2>&1 || true
        DEBIAN_FRONTEND=noninteractive apt-get install -y -qq dialog >/dev/null 2>&1 || true
    fi
    if ! command -v dialog >/dev/null 2>&1; then
        print_warning "'dialog' not available — using classic output"
        return 0
    fi

    # dialog needs a sane window to draw in; anything smaller just errors out.
    local rows cols
    read -r rows cols < <(stty size </dev/tty 2>/dev/null || echo "0 0")
    if [ "${cols:-0}" -lt 70 ] || [ "${rows:-0}" -lt 20 ]; then
        print_warning "Terminal too small for the installer UI — using classic output"
        return 0
    fi

    TUI=true
}

# tui_cancel: clean exit when the operator backs out of any pre-install screen
# (ESC or Cancel). Nothing has been touched yet at that point, so exit 0.
tui_cancel() {
    clear
    echo "Installation cancelled."
    exit 0
}

# tui_welcome: splash screen shown before the first question.
tui_welcome() {
    dlg --title "Welcome" --msgbox \
        "Welcome to the ProxmoxVEx installer.\n\nThis wizard will install the ProxmoxVEx cluster\nmanagement platform on this host and register\nit as a systemd service.\n\nPress ENTER to continue, or ESC to cancel." \
        13 64 || tui_cancel
}

# tui_ask_port: menu-based port picker. Skipped entirely when --port pinned
# the port on the command line — the question is already answered.
tui_ask_port() {
    [ "$PORT_VIA_ARG" = true ] && return 0
    local choice
    while true; do
        choice=$(dlg --title "Web Interface Port" --menu \
            "Choose which port the web UI should listen on:" \
            13 64 3 \
            "5000"   "Standard port (recommended)" \
            "443"    "HTTPS on the standard port" \
            "custom" "Enter a custom port (1-65535)") || tui_cancel
        case "$choice" in
            5000|443)
                ACCESS_PORT=$choice
                break
                ;;
            custom)
                # Cancel on the input box goes back to the menu, not out.
                choice=$(dlg --title "Custom Port" --inputbox \
                    "Enter the port for the web UI (1-65535):" \
                    9 60 "$ACCESS_PORT") || continue
                if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le 65535 ]; then
                    ACCESS_PORT=$choice
                    break
                fi
                dlg --title "Invalid Port" --msgbox \
                    "'$choice' is not a valid port. Enter a number between 1 and 65535." \
                    8 62 || true
                ;;
        esac
    done
}

# tui_ask_options: optional-component checklist. Each item's default follows
# its flag/env var so a CLI --no-offline or PROXMOXVEX_CI_TOOLS=0 still wins.
tui_ask_options() {
    local off_state="ON" ci_state="ON" opts
    [ "$DOWNLOAD_OFFLINE" = true ] || off_state="OFF"
    [ "$INSTALL_CI_TOOLS" = true ] || ci_state="OFF"
    opts=$(dlg --title "Install Options" --checklist \
        "Toggle optional components (SPACE to select):" \
        13 68 2 \
        "offline" "Download web assets for offline use" "$off_state" \
        "citools" "CI/release tools (helm, trivy, hadolint)" "$ci_state") || tui_cancel
    case "$opts" in
        *offline*) ;;
        *) DOWNLOAD_OFFLINE=false ;;
    esac
    case "$opts" in
        *citools*) ;;
        *) INSTALL_CI_TOOLS=false ;;
    esac
}

# tui_confirm: last gate before any system change — recap the answers.
tui_confirm() {
    local offline="no" citools="no"
    [ "$DOWNLOAD_OFFLINE" = true ] && offline="yes"
    [ "$INSTALL_CI_TOOLS" = true ] && citools="yes"
    if ! dlg --title "Ready to Install" --yesno \
        "Review the installation settings:\n\n  Install dir    : $INSTALL_DIR\n  Python         : ${DEP_PY_VER:-unknown}  ($PYTHON_BIN)\n  Web port       : $ACCESS_PORT  (VNC $((ACCESS_PORT+1)), SSH $((ACCESS_PORT+2)))\n  Offline assets : $offline\n  CI tools       : $citools\n\nProceed with the installation?" \
        15 66; then
        tui_cancel
    fi
}

# tui_success / tui_failure: closing screens. Both drop a plain-text summary
# on the terminal afterwards so the URLs and commands survive in the SSH
# scrollback after the curses screen is cleared.
tui_success() {
    local ip urls
    ip=$(hostname -I 2>/dev/null | awk '{print $1}')
    [ -z "$ip" ] && ip="<your-ip>"
    if [ "$ACCESS_PORT" = 443 ]; then
        urls="Web Interface : https://$ip\nVNC WebSocket : https://$ip:444\nSSH WebSocket : https://$ip:445"
    else
        urls="Web Interface : https://$ip:$ACCESS_PORT\nVNC WebSocket : https://$ip:$((ACCESS_PORT+1))\nSSH WebSocket : https://$ip:$((ACCESS_PORT+2))"
    fi
    dlg --title "Installation Complete" --msgbox \
        "ProxmoxVEx is installed and running.\n\n$urls\n\nService : systemctl status ProxmoxVEx\nLogs    : journalctl -u ProxmoxVEx -f\nLog file: $TUI_LOG" \
        16 66 || true
    clear
    print_final_summary
    echo -e "${CYAN}Install log: $TUI_LOG${NC}"
}

tui_failure() {
    dlg --title "Installation Failed" --msgbox \
        "The installation did not complete cleanly.\n\nThe install log is shown next for diagnosis.\nPress ESC or q to close it." \
        11 64 || true
    dlg --textbox "$TUI_LOG" "$TUI_PROG_H" "$TUI_PROG_W" || true
    clear
    print_error "Installation failed — see $TUI_LOG"
    exit 1
}

# tui_main: the TUI flow — collect every answer up front, then run the
# installer with its output streamed into a live --progressbox window and
# mirrored to $TUI_LOG for the failure viewer. Answers land in the same
# variables the classic prompts write, and INTERACTIVE is cleared so no
# mid-install prompt can fight dialog for the terminal; run_install also gets
# /dev/null on stdin as a second guard, because any stray `read` would block
# forever behind the curses screen.
tui_main() {
    tui_welcome

    # Fall back to a temp file if /var/log isn't writable for some reason.
    # Created up front so the dependency steps can log into it too.
    : > "$TUI_LOG" 2>/dev/null || TUI_LOG=$(mktemp /tmp/proxmoxvex-install.XXXXXX.log)

    # Dependency detection: report → guided install → interpreter decision.
    tui_deps_report
    tui_deps_install
    tui_python_resolve

    tui_ask_port
    tui_ask_options
    tui_confirm

    INTERACTIVE=false

    # tee -a: the dependency steps above already logged into $TUI_LOG.
    run_install </dev/null 2>&1 | tee -a "$TUI_LOG" | \
        dialog --backtitle "$TUI_BACKTITLE" \
            --title "Installing ProxmoxVEx" \
            --progressbox "Live installer output — do not close this terminal" \
            "$TUI_PROG_H" "$TUI_PROG_W"
    local rc=${PIPESTATUS[0]}

    # The pipeline's exit status is dialog's; the installer's own result lives
    # in PIPESTATUS[0]. Double-check the service too — run_install prints an
    # error but exits 0 when the unit fails to come up.
    if [ "$rc" -eq 0 ] && systemctl is-active --quiet ProxmoxVEx; then
        tui_success
    else
        tui_failure
    fi
}

# =============================================================================
# Dependency Detection & Guided Setup
# Sep 2026 — before touching the system, scan every prerequisite the install
# needs (package manager, interpreter + version, venv/pip modules, system
# tools, disk space) and report the result. Whatever is missing gets installed
# step by step, prompting only where an answer genuinely can't be derived —
# e.g. which Python interpreter to use. Details collected here feed straight
# into the application configuration (venv interpreter, port, optional
# components), so nothing is asked twice.
# =============================================================================
DEP_MISSING_PKGS=()
DEP_MISSING_LABELS=()
DEP_PY_STATE=""        # ok | missing | old | new | broken
DEP_PY_VER=""
DEP_PY_VENV_BAD=false  # interpreter present but lacks venv/ensurepip
DEP_APT=true
DEP_DISK_FREE_MB=0
DEP_DISK_OK=true
DEP_DISK_MIN_MB=400

_dep_need() {
    DEP_MISSING_LABELS+=("$1")
    DEP_MISSING_PKGS+=("$2")
}

# dep_collect_missing: populate DEP_MISSING_PKGS/LABELS with the apt packages
# still needed. Pure detection — no side effects — so both UIs and the
# re-scan after a guided install share one source of truth.
dep_collect_missing() {
    DEP_MISSING_PKGS=()
    DEP_MISSING_LABELS=()
    DEP_PY_VENV_BAD=false
    DEP_APT=true
    command -v apt-get >/dev/null 2>&1 || DEP_APT=false

    if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
        # A missing distro python3 maps to apt packages; a missing custom
        # interpreter can't be fixed by apt — the resolver handles that.
        if [ "$PYTHON_BIN" = "python3" ]; then
            _dep_need "Python 3 interpreter" python3
            _dep_need "Python pip module" python3-pip
            _dep_need "Python venv module" python3-venv
        fi
    elif [ "$PYTHON_BIN" = "python3" ]; then
        "$PYTHON_BIN" -m pip --version >/dev/null 2>&1 || _dep_need "Python pip module" python3-pip
        "$PYTHON_BIN" -c 'import venv, ensurepip' >/dev/null 2>&1 || _dep_need "Python venv module" python3-venv
    else
        # Custom interpreter: apt can't supply its modules — flag instead.
        "$PYTHON_BIN" -c 'import venv, ensurepip' >/dev/null 2>&1 || DEP_PY_VENV_BAD=true
    fi

    local cmd
    for cmd in curl wget git openssl sshpass sqlite3 sudo; do
        command -v "$cmd" >/dev/null 2>&1 || _dep_need "$cmd" "$cmd"
    done
    [ -d /etc/ssl/certs ] || _dep_need "CA certificates" ca-certificates
}

# dep_python_state: classify $PYTHON_BIN into DEP_PY_STATE + DEP_PY_VER.
dep_python_state() {
    DEP_PY_STATE="broken"
    DEP_PY_VER=""
    command -v "$PYTHON_BIN" >/dev/null 2>&1 || { DEP_PY_STATE="missing"; return 0; }
    DEP_PY_VER=$("$PYTHON_BIN" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
    [ -n "$DEP_PY_VER" ] || return 0
    local maj=${DEP_PY_VER%%.*} min=${DEP_PY_VER#*.}
    case "$min" in ''|*[!0-9]*) return 0 ;; esac
    if [ "$maj" != "3" ] || [ "$min" -lt 10 ]; then
        DEP_PY_STATE="old"
    elif [ "$min" -ge 14 ]; then
        DEP_PY_STATE="new"
    else
        DEP_PY_STATE="ok"
    fi
}

# dep_disk_check: venv + source + offline assets need a few hundred MB.
dep_disk_check() {
    DEP_DISK_FREE_MB=$(df -Pm /opt 2>/dev/null | awk 'NR==2{print $4}')
    case "$DEP_DISK_FREE_MB" in ''|*[!0-9]*)
        DEP_DISK_FREE_MB=$(df -Pm / 2>/dev/null | awk 'NR==2{print $4}')
        ;;
    esac
    case "$DEP_DISK_FREE_MB" in ''|*[!0-9]*) DEP_DISK_FREE_MB=0 ;; esac
    [ "$DEP_DISK_FREE_MB" -ge "$DEP_DISK_MIN_MB" ] && DEP_DISK_OK=true || DEP_DISK_OK=false
}

# check_connectivity: probe curl first — ICMP echo is filtered on plenty of
# networks, and minimal images often ship curl but not iputils-ping (the old
# ping-only check wrongly reported "no internet" there). Returns 0=online,
# 1=offline, 2=cannot determine (no probe tool installed yet).
check_connectivity() {
    local curl_ok=false ping_ok=false
    command -v curl >/dev/null 2>&1 && curl_ok=true
    command -v ping >/dev/null 2>&1 && ping_ok=true
    if [ "$curl_ok" = true ]; then
        curl -fsS -m 8 -o /dev/null https://proxmoxvex.com 2>/dev/null && return 0
    fi
    if [ "$ping_ok" = true ]; then
        ping -c 1 -W 3 proxmoxvex.com >/dev/null 2>&1 && return 0
    fi
    [ "$curl_ok" = false ] && [ "$ping_ok" = false ] && return 2
    return 1
}

# dep_step: run one dependency step inside a titled live-output window so the
# operator watches each prerequisite being resolved instead of a text wall.
dep_step() {
    local title="$1" desc="$2"
    shift 2
    "$@" 2>&1 | tee -a "$TUI_LOG" | \
        dialog --backtitle "$TUI_BACKTITLE" --title "Dependencies — $title" \
            --progressbox "$desc" 14 "$TUI_PROG_W"
    return "${PIPESTATUS[0]}"
}

# tui_deps_report: status table — what's satisfied, what will be installed,
# what needs a decision — gated on a single Continue.
tui_deps_report() {
    if [ "$DEP_APT" = false ]; then
        dlg --title "Unsupported Platform" --msgbox \
            "This installer needs a Debian/Ubuntu-style system with apt-get.\n\nNo supported package manager was found on this host.\nSee https://proxmoxvex.com/docs for manual installation." \
            12 68 || true
        tui_hard_fail
    fi

    local txt="Dependency check results:\n" i
    txt+="\n  ✓ apt package manager"
    txt+="\n  ✓ Internet connectivity"
    case "$DEP_PY_STATE" in
        ok)      txt+="\n  ✓ Python $DEP_PY_VER ($PYTHON_BIN)" ;;
        missing) txt+="\n  → Python 3 — will be installed" ;;
        old)     txt+="\n  ✗ Python $DEP_PY_VER — too old (need 3.10–3.13)" ;;
        new)     txt+="\n  ⚠ Python $DEP_PY_VER — newer than tested (3.10–3.13)" ;;
        *)       txt+="\n  ✗ $PYTHON_BIN — unusable interpreter" ;;
    esac
    [ "$DEP_PY_VENV_BAD" = true ] && txt+="\n  ✗ $PYTHON_BIN lacks venv/ensurepip"
    for i in "${!DEP_MISSING_LABELS[@]}"; do
        # The interpreter line above already covers the python3 package.
        [ "${DEP_MISSING_LABELS[$i]}" = "Python 3 interpreter" ] && continue
        txt+="\n  → ${DEP_MISSING_LABELS[$i]} — will be installed"
    done
    if [ "$DEP_DISK_OK" = true ]; then
        txt+="\n  ✓ Disk space (${DEP_DISK_FREE_MB} MB free)"
    else
        txt+="\n  ⚠ Disk space low (${DEP_DISK_FREE_MB} MB free, ${DEP_DISK_MIN_MB}+ recommended)"
    fi

    local prompt="All prerequisites satisfied. Continue?"
    if [ ${#DEP_MISSING_PKGS[@]} -gt 0 ]; then
        prompt="Missing items will be installed step by step. Continue?"
    fi
    dlg --title "Dependency Check" --yesno "$txt\n\n$prompt" 20 68 || tui_cancel
}

# tui_deps_install: guided step-by-step install of the missing apt packages,
# split into logical steps so each progress window says what is happening.
tui_deps_install() {
    [ ${#DEP_MISSING_PKGS[@]} -eq 0 ] && return 0

    local p py=() tools=()
    for p in "${DEP_MISSING_PKGS[@]}"; do
        case "$p" in
            python3*) py+=("$p") ;;
            *)        tools+=("$p") ;;
        esac
    done

    dep_step "Package lists" "Refreshing apt package lists..." \
        env DEBIAN_FRONTEND=noninteractive apt-get update -qq || true
    if [ ${#py[@]} -gt 0 ]; then
        dep_step "Python runtime" "Installing: ${py[*]}" \
            env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${py[@]}" \
            || { dlg --msgbox "Failed to install: ${py[*]}\n\nSee $TUI_LOG for details." 10 64 || true; tui_hard_fail; }
    fi
    if [ ${#tools[@]} -gt 0 ]; then
        dep_step "System tools" "Installing: ${tools[*]}" \
            env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${tools[@]}" \
            || { dlg --msgbox "Failed to install: ${tools[*]}\n\nSee $TUI_LOG for details." 10 64 || true; tui_hard_fail; }
    fi

    # Re-scan — everything queued above should now show as satisfied.
    dep_collect_missing
    dep_python_state
    if [ ${#DEP_MISSING_PKGS[@]} -gt 0 ]; then
        dlg --title "Dependencies" --msgbox \
            "Some prerequisites could not be installed:\n\n  ${DEP_MISSING_LABELS[*]}\n\nSee $TUI_LOG for details." 14 66 || true
        tui_hard_fail
    fi
}

# _py_candidate_ok: validate a user-supplied interpreter path. Sets
# PYTHON_CANDIDATE to the resolved binary on success, or _PY_CANDIDATE_ERR
# with a human-readable reason. venv+ensurepip are required because the app
# builds its own virtualenv at install time.
_py_candidate_ok() {
    _PY_CANDIDATE_ERR=""
    PYTHON_CANDIDATE=""
    local p="$1" v maj min
    [ -n "$p" ] || { _PY_CANDIDATE_ERR="No path given."; return 1; }
    [ -x "$p" ] || p=$(command -v "$p" 2>/dev/null) \
        || { _PY_CANDIDATE_ERR="'$1' is not executable and not on PATH."; return 1; }
    v=$("$p" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null) \
        || { _PY_CANDIDATE_ERR="'$p' did not run — not a Python interpreter?"; return 1; }
    maj=${v%%.*}; min=${v#*.}
    case "$min" in ''|*[!0-9]*) min=0 ;; esac
    if [ "$maj" != "3" ] || [ "$min" -lt 10 ] || [ "$min" -ge 14 ]; then
        _PY_CANDIDATE_ERR="'$p' is Python $v — ProxmoxVEx needs 3.10–3.13."
        return 1
    fi
    "$p" -c 'import venv, ensurepip' >/dev/null 2>&1 \
        || { _PY_CANDIDATE_ERR="'$p' lacks venv/ensurepip — the app cannot build its virtualenv."; return 1; }
    PYTHON_CANDIDATE=$p
    return 0
}

# tui_python_resolve: the one prerequisite that can need a real answer —
# which interpreter to build the app on. Auto-resolves via apt where possible
# and only asks when a decision is genuinely required. The chosen interpreter
# is what run_install builds the venv with, so the answer flows straight into
# the application's runtime configuration.
tui_python_resolve() {
    local choice path msg mh
    local -a items
    while true; do
        dep_python_state
        [ "$DEP_PY_STATE" = ok ] && [ "$DEP_PY_VENV_BAD" = false ] && return 0

        if [ "$DEP_PY_STATE" = new ]; then
            if ! dlg --title "Unsupported Python Version" --yesno \
                "Python $DEP_PY_VER is newer than what ProxmoxVEx is tested on (3.10–3.13).\n\nKnown issue on 3.14: SSH/VNC WebSocket subprocesses may\nfail to bind cleanly (issue #388).\n\nContinue anyway?" \
                13 64; then
                tui_cancel
            fi
            DEPLOY_FORCE_PY=1
            return 0
        fi

        case "$DEP_PY_STATE" in
            old)     msg="Python $DEP_PY_VER is too old — ProxmoxVEx needs 3.10–3.13." ;;
            missing) msg="No Python 3 interpreter was found." ;;
            *)       msg="$PYTHON_BIN is not a usable interpreter." ;;
        esac
        [ "$DEP_PY_VENV_BAD" = true ] && msg="$PYTHON_BIN has no venv/ensurepip support."

        items=()
        [ "$DEP_APT" = true ] && items+=("auto" "Install via apt (recommended)")
        items+=("path" "Use a different interpreter (custom path)")
        items+=("abort" "Cancel the installation")
        mh=$(( ${#items[@]} / 2 ))
        choice=$(dlg --title "Python Interpreter" --menu \
            "$msg\n\nHow should Python be provided?" \
            $(( 9 + mh )) 68 "$mh" "${items[@]}") || tui_cancel

        case "$choice" in
            auto)
                dep_step "Python runtime" "Installing Python 3 via apt..." \
                    env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
                    python3 python3-pip python3-venv || true
                # A custom PYTHON_BIN that apt can't provide falls back to
                # the freshly installed distro interpreter.
                if ! command -v "$PYTHON_BIN" >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then
                    PYTHON_BIN=python3
                fi
                dep_python_state
                dep_collect_missing
                if [ "$DEP_PY_STATE" != ok ] && [ "$DEP_PY_STATE" != new ]; then
                    dlg --msgbox \
                        "apt did not provide a suitable interpreter (found: ${DEP_PY_VER:-none}).\n\nTry the custom-path option, or install Python 3.10–3.13 manually." \
                        11 66 || true
                fi
                ;;
            path)
                path=$(dlg --title "Python Interpreter" --inputbox \
                    "Full path to a Python 3.10–3.13 binary:" 9 62) || continue
                if _py_candidate_ok "$path"; then
                    PYTHON_BIN=$PYTHON_CANDIDATE
                    dep_collect_missing
                    dep_python_state
                else
                    dlg --msgbox "$_PY_CANDIDATE_ERR" 10 64 || true
                fi
                ;;
            abort)
                tui_cancel
                ;;
        esac
    done
}

# tui_hard_fail: unrecoverable prerequisite failure — clear the curses screen
# and leave a plain error line behind.
tui_hard_fail() {
    clear
    print_error "Installation aborted — see $TUI_LOG for details"
    exit 1
}

# dep_classic_report: the classic-mode equivalent of the dependency screen —
# same scan data, printed as status lines. Version problems still get a way
# out (interpreter path prompt) when a terminal is attached.
dep_classic_report() {
    if [ "$DEP_APT" = false ]; then
        print_error "No supported package manager (apt-get) found — ProxmoxVEx deploy requires Debian/Ubuntu."
        exit 1
    fi
    if [ ${#DEP_MISSING_PKGS[@]} -gt 0 ]; then
        print_info "Missing dependencies (will be installed): ${DEP_MISSING_LABELS[*]}"
    else
        print_info "All system dependencies already satisfied"
    fi
    case "$DEP_PY_STATE" in
        old)
            print_error "Python $DEP_PY_VER is too old — ProxmoxVEx needs 3.10–3.13."
            if [ "$INTERACTIVE" = true ] && [ -t 0 ]; then
                local _pyp
                read -r -p "Path to a Python 3.10–3.13 interpreter (Enter to abort): " _pyp < /dev/tty
                if [ -n "$_pyp" ] && _py_candidate_ok "$_pyp"; then
                    PYTHON_BIN=$PYTHON_CANDIDATE
                    print_success "Using interpreter $PYTHON_BIN"
                    dep_collect_missing
                    dep_python_state
                else
                    [ -n "$_pyp" ] && print_error "$_PY_CANDIDATE_ERR"
                    exit 1
                fi
            else
                exit 1
            fi
            ;;
        broken)
            print_error "$PYTHON_BIN is not a usable interpreter"
            exit 1
            ;;
        missing)
            [ "$DEP_APT" = true ] || { print_error "python3 missing and no apt available to install it"; exit 1; }
            print_info "python3 will be installed via apt"
            ;;
    esac
    if [ "$DEP_PY_VENV_BAD" = true ]; then
        print_error "$PYTHON_BIN lacks venv/ensurepip — cannot build the virtualenv"
        exit 1
    fi
    [ "$DEP_DISK_OK" = false ] && \
        print_warning "Low disk space: ${DEP_DISK_FREE_MB} MB free (${DEP_DISK_MIN_MB}+ MB recommended)"
}

# =============================================================================
# Install CI/Release Tooling
# Helm, Trivy, and hadolint are required by the packaging pipelines so the
# Docker lint/scan and Helm lint/template/sign/scan stages do not get skipped.
# Installs only the tools that are not already present on the host.
# =============================================================================
install_ci_tools() {
    local arch
    arch=$(uname -m)
    local hadolint_arch="$arch"
    case "$arch" in
        x86_64) hadolint_arch="x86_64" ;;
        aarch64|arm64) hadolint_arch="arm64" ;;
        *) echo "Unsupported architecture: $arch" >&2; return 1 ;;
    esac

    if ! command -v helm >/dev/null 2>&1; then
        print_info "Installing Helm 3..."
        curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
        print_success "Helm installed"
    else
        print_info "Helm already installed"
    fi

    if ! command -v trivy >/dev/null 2>&1; then
        print_info "Installing Trivy..."
        curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
        print_success "Trivy installed"
    else
        print_info "Trivy already installed"
    fi

    if ! command -v hadolint >/dev/null 2>&1; then
        print_info "Installing hadolint..."
        curl -fL -o /usr/local/bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-${hadolint_arch}"
        chmod +x /usr/local/bin/hadolint
        print_success "hadolint installed"
    else
        print_info "hadolint already installed"
    fi
}

# =============================================================================
# Main Installation Routine
# Split from main() so it can run either directly (classic output) or inside
# the TUI's live-output window (tui_main). Root, connectivity and UI selection
# all happen in main() before this is invoked.
# =============================================================================
run_install() {
    # =========================================================================
    # Step 1: System Dependencies
    # Install the Debian packages that ProxmoxVEx and its Python stack need.
    # =========================================================================
    print_step "Step 1/6: Installing System Dependencies"

    # Avoid any interactive debconf prompts during package installation.
    export DEBIAN_FRONTEND=noninteractive

    # Re-scan rather than installing a fixed list — the dependency pass in
    # main()/tui_main usually resolved everything already, so this only
    # installs what is genuinely still missing.
    dep_collect_missing
    if [ ${#DEP_MISSING_PKGS[@]} -gt 0 ]; then
        apt-get update -qq
        print_info "Installing missing packages: ${DEP_MISSING_LABELS[*]}"
        apt-get install -y -qq "${DEP_MISSING_PKGS[@]}" > /dev/null 2>&1
    else
        print_info "All required packages already present"
    fi

    print_success "System dependencies installed"

    # Install the packaging/CI tooling (helm, trivy, hadolint) so pipelines
    # can lint, sign, and scan instead of silently skipping their stages.
    # Optional — the TUI options checklist / PROXMOXVEX_CI_TOOLS can skip it.
    if [ "$INSTALL_CI_TOOLS" = true ]; then
        install_ci_tools
    else
        print_info "Skipping CI/release tools (disabled in install options)"
    fi

    # =========================================================================
    # Step 2: Create User & Directories
    # Prepare the service account and the on-disk layout for runtime data.
    # =========================================================================
    print_step "Step 2/6: Creating User & Directories"

    # Create the ProxmoxVEx service user if it does not already exist.
    # --system gives it a low UID and no login; --no-create-home --shell /bin/false
    # ensures it cannot be used for an interactive session.
    if id "$SERVICE_USER" &>/dev/null; then
        print_info "Service user '$SERVICE_USER' already exists"
    else
        useradd --system --no-create-home --shell /bin/false "$SERVICE_USER"
        print_success "Service user '$SERVICE_USER' created"
    fi

    # Create the working directory and all subdirectories in one shot.
    mkdir -p "$INSTALL_DIR"/{config,logs,ssl,static,web,images,backups}
    print_success "Directory structure created"

    # =========================================================================
    # Step 3: Download ProxmoxVEx
    # Acquire the source code. Prefer a local checkout (e.g. the directory
    # containing this deploy.sh) so developers can install from the repo they
    # are already in; otherwise fetch the release archive from the main site
    # (or clone PROXMOXVEX_REPO when a full-source git remote is configured).
    # =========================================================================
    print_step "Step 3/6: Downloading ProxmoxVEx"

    # NS: feb 2026 - detect if running from a checkout that already has the files
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo "")"

    if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/$PYTHON_FILE" ] && [ -d "$SCRIPT_DIR/ProxmoxVEx" ]; then
        # Running from existing checkout - copy directly instead of cloning
        print_info "Found local installation files in $SCRIPT_DIR"

        # Only copy if the checkout is not already the install directory itself.
        if [ "$SCRIPT_DIR" != "$INSTALL_DIR" ]; then
            cp "$SCRIPT_DIR/$PYTHON_FILE" "$INSTALL_DIR/"
            cp -r "$SCRIPT_DIR/ProxmoxVEx" "$INSTALL_DIR/"
            [ -d "$SCRIPT_DIR/web" ] && cp -r "$SCRIPT_DIR/web/"* "$INSTALL_DIR/web/" 2>/dev/null || true
            [ -d "$SCRIPT_DIR/images" ] && cp -r "$SCRIPT_DIR/images/"* "$INSTALL_DIR/images/" 2>/dev/null || true
            [ -d "$SCRIPT_DIR/static" ] && cp -r "$SCRIPT_DIR/static/"* "$INSTALL_DIR/static/" 2>/dev/null || true
            [ -f "$SCRIPT_DIR/version.json" ] && cp "$SCRIPT_DIR/version.json" "$INSTALL_DIR/"
            [ -f "$SCRIPT_DIR/requirements.txt" ] && cp "$SCRIPT_DIR/requirements.txt" "$INSTALL_DIR/"
            [ -f "$SCRIPT_DIR/update.sh" ] && cp "$SCRIPT_DIR/update.sh" "$INSTALL_DIR/"
            [ -f "$SCRIPT_DIR/deploy.sh" ] && cp "$SCRIPT_DIR/deploy.sh" "$INSTALL_DIR/"
        fi

        print_success "Files copied from local checkout"
    else
        TEMP_DIR=$(mktemp -d)

        if [ -n "$SOURCE_REPO" ]; then
            # Internal/dev path: clone a full-source git remote.
            print_info "Cloning repository..."
            if git clone --depth 1 --branch "$GITHUB_BRANCH" --quiet "$SOURCE_REPO" "$TEMP_DIR/ProxmoxVEx" 2>/dev/null; then
                print_success "Repository cloned (branch: $GITHUB_BRANCH)"
            else
                print_error "Failed to clone repository"
                rm -rf "$TEMP_DIR"
                exit 1
            fi
        else
            # Public path: the release archive ships the full file tree.
            print_info "Downloading release archive..."
            mkdir -p "$TEMP_DIR/ProxmoxVEx"
            if ! curl -fsSL "$RELEASE_ARCHIVE" -o "$TEMP_DIR/ProxmoxVEx.tar.gz" 2>/dev/null; then
                print_error "Failed to download release archive"
                rm -rf "$TEMP_DIR"
                exit 1
            fi
            if ! tar -xzf "$TEMP_DIR/ProxmoxVEx.tar.gz" -C "$TEMP_DIR/ProxmoxVEx" 2>/dev/null; then
                print_error "Failed to extract release archive"
                rm -rf "$TEMP_DIR"
                exit 1
            fi
            print_success "Release archive downloaded"
        fi

        # Copy ALL files from the acquired tree
        cp -r "$TEMP_DIR/ProxmoxVEx/"* "$INSTALL_DIR/" 2>/dev/null || true

        # Move index.html to web folder if exists in root
        [ -f "$INSTALL_DIR/index.html" ] && mv "$INSTALL_DIR/index.html" "$INSTALL_DIR/web/" 2>/dev/null || true

        # Remove git folder
        rm -rf "$INSTALL_DIR/.git" 2>/dev/null || true

        print_success "All files copied to $INSTALL_DIR"
        rm -rf "$TEMP_DIR"
    fi

    # Make the helper scripts executable so they can be invoked by the app user.
    chmod +x "$INSTALL_DIR/deploy.sh" "$INSTALL_DIR/update.sh" 2>/dev/null || true

    # If the canary template pack was requested, stage it in the install tree.
    if [ "$CANARY" = true ]; then
        print_info "Staging canary template pack..."
        if [ -d "$INSTALL_DIR/packaging/canary" ]; then
            cp -r "$INSTALL_DIR/packaging/canary" "$INSTALL_DIR/canary-template-pack" 2>/dev/null || true
            print_success "Canary template pack staged at $INSTALL_DIR/canary-template-pack"
        else
            print_warning "Canary template pack not found in source"
        fi
    fi

    # If the canary replicated service pack was requested, stage it in the install tree.
    if [ "$CANARY_REPLICATED" = true ]; then
        print_info "Staging canary replicated service pack..."
        if [ -d "$INSTALL_DIR/packaging/canary-replicated-service" ]; then
            cp -r "$INSTALL_DIR/packaging/canary-replicated-service" "$INSTALL_DIR/canary-replicated-service" 2>/dev/null || true
            print_success "Canary replicated service pack staged at $INSTALL_DIR/canary-replicated-service"
        else
            print_warning "Canary replicated service pack not found in source"
        fi
    fi

    # If the canary signed image pack was requested, stage it in the install tree.
    if [ "$CANARY_SIGNED" = true ]; then
        print_info "Staging canary signed image pack..."
        if [ -d "$INSTALL_DIR/packaging/canary-signed-images" ]; then
            cp -r "$INSTALL_DIR/packaging/canary-signed-images" "$INSTALL_DIR/canary-signed-images" 2>/dev/null || true
            print_success "Canary signed image pack staged at $INSTALL_DIR/canary-signed-images"
        else
            print_warning "Canary signed image pack not found in source"
        fi
    fi

    # If the canary vulnerability scan pack was requested, stage it in the install tree.
    if [ "$CANARY_VULNERABILITY_SCAN" = true ]; then
        print_info "Staging canary vulnerability scan pack..."
        if [ -d "$INSTALL_DIR/packaging/canary-vulnerability-scan" ]; then
            cp -r "$INSTALL_DIR/packaging/canary-vulnerability-scan" "$INSTALL_DIR/canary-vulnerability-scan" 2>/dev/null || true
            print_success "Canary vulnerability scan pack staged at $INSTALL_DIR/canary-vulnerability-scan"
        else
            print_warning "Canary vulnerability scan pack not found in source"
        fi
    fi

    # If the canary CI pipeline pack was requested, stage it in the install tree.
    if [ "$CANARY_CI" = true ]; then
        print_info "Staging canary CI pipeline pack..."
        if [ -d "$INSTALL_DIR/packaging/canary-ci-pipeline" ]; then
            cp -r "$INSTALL_DIR/packaging/canary-ci-pipeline" "$INSTALL_DIR/canary-ci-pipeline" 2>/dev/null || true
            print_success "Canary CI pipeline pack staged at $INSTALL_DIR/canary-ci-pipeline"
        else
            print_warning "Canary CI pipeline pack not found in source"
        fi
    fi

    # If the canary CD pipeline pack was requested, stage it in the install tree.
    if [ "$CANARY_CD" = true ]; then
        print_info "Staging canary CD pipeline pack..."
        if [ -d "$INSTALL_DIR/packaging/canary-cd-pipeline" ]; then
            cp -r "$INSTALL_DIR/packaging/canary-cd-pipeline" "$INSTALL_DIR/canary-cd-pipeline" 2>/dev/null || true
            print_success "Canary CD pipeline pack staged at $INSTALL_DIR/canary-cd-pipeline"
        else
            print_warning "Canary CD pipeline pack not found in source"
        fi
    fi

    # If the canary upgrade path pack was requested, stage it in the install tree.
    if [ "$CANARY_UPGRADE" = true ]; then
        print_info "Staging canary upgrade path pack..."
        if [ -d "$INSTALL_DIR/packaging/canary-upgrade-path" ]; then
            cp -r "$INSTALL_DIR/packaging/canary-upgrade-path" "$INSTALL_DIR/canary-upgrade-path" 2>/dev/null || true
            print_success "Canary upgrade path pack staged at $INSTALL_DIR/canary-upgrade-path"
        else
            print_warning "Canary upgrade path pack not found in source"
        fi
    fi

    # If the canary rollback path pack was requested, stage it in the install tree.
    if [ "$CANARY_ROLLBACK" = true ]; then
        print_info "Staging canary rollback path pack..."
        if [ -d "$INSTALL_DIR/packaging/canary-rollback-path" ]; then
            cp -r "$INSTALL_DIR/packaging/canary-rollback-path" "$INSTALL_DIR/canary-rollback-path" 2>/dev/null || true
            print_success "Canary rollback path pack staged at $INSTALL_DIR/canary-rollback-path"
        else
            print_warning "Canary rollback path pack not found in source"
        fi
    fi

    # If the rolling base chart pack was requested, stage it in the install tree.
    if [ "$ROLLING_BASE" = true ]; then
        print_info "Staging rolling base chart pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-base-chart" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-base-chart" "$INSTALL_DIR/rolling-base-chart" 2>/dev/null || true
            print_success "Rolling base chart pack staged at $INSTALL_DIR/rolling-base-chart"
        else
            print_warning "Rolling base chart pack not found in source"
        fi
    fi

    # If the rolling multi-arch build pack was requested, stage it in the install tree.
    if [ "$ROLLING_MULTI_ARCH" = true ]; then
        print_info "Staging rolling multi-arch build pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-multi-arch-build" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-multi-arch-build" "$INSTALL_DIR/rolling-multi-arch-build" 2>/dev/null || true
            print_success "Rolling multi-arch build pack staged at $INSTALL_DIR/rolling-multi-arch-build"
        else
            print_warning "Rolling multi-arch build pack not found in source"
        fi
    fi

    # If the rolling template pack was requested, stage it in the install tree.
    if [ "$ROLLING_TEMPLATE" = true ]; then
        print_info "Staging rolling template pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-template-pack" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-template-pack" "$INSTALL_DIR/rolling-template-pack" 2>/dev/null || true
            print_success "Rolling template pack staged at $INSTALL_DIR/rolling-template-pack"
        else
            print_warning "Rolling template pack not found in source"
        fi
    fi

    # If the rolling replicated service pack was requested, stage it in the install tree.
    if [ "$ROLLING_REPLICATED" = true ]; then
        print_info "Staging rolling replicated service pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-replicated-service" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-replicated-service" "$INSTALL_DIR/rolling-replicated-service" 2>/dev/null || true
            print_success "Rolling replicated service pack staged at $INSTALL_DIR/rolling-replicated-service"
        else
            print_warning "Rolling replicated service pack not found in source"
        fi
    fi

    # If the rolling signed images pack was requested, stage it in the install tree.
    if [ "$ROLLING_SIGNED" = true ]; then
        print_info "Staging rolling signed images pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-signed-images" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-signed-images" "$INSTALL_DIR/rolling-signed-images" 2>/dev/null || true
            print_success "Rolling signed images pack staged at $INSTALL_DIR/rolling-signed-images"
        else
            print_warning "Rolling signed images pack not found in source"
        fi
    fi

    # If the rolling vulnerability scan pack was requested, stage it in the install tree.
    if [ "$ROLLING_VULNERABILITY" = true ]; then
        print_info "Staging rolling vulnerability scan pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-vulnerability-scan" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-vulnerability-scan" "$INSTALL_DIR/rolling-vulnerability-scan" 2>/dev/null || true
            print_success "Rolling vulnerability scan pack staged at $INSTALL_DIR/rolling-vulnerability-scan"
        else
            print_warning "Rolling vulnerability scan pack not found in source"
        fi
    fi

    # If the rolling CI pipeline pack was requested, stage it in the install tree.
    if [ "$ROLLING_CI" = true ]; then
        print_info "Staging rolling CI pipeline pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-ci-pipeline" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-ci-pipeline" "$INSTALL_DIR/rolling-ci-pipeline" 2>/dev/null || true
            print_success "Rolling CI pipeline pack staged at $INSTALL_DIR/rolling-ci-pipeline"
        else
            print_warning "Rolling CI pipeline pack not found in source"
        fi
    fi

    # If the rolling CD pipeline pack was requested, stage it in the install tree.
    if [ "$ROLLING_CD" = true ]; then
        print_info "Staging rolling CD pipeline pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-cd-pipeline" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-cd-pipeline" "$INSTALL_DIR/rolling-cd-pipeline" 2>/dev/null || true
            print_success "Rolling CD pipeline pack staged at $INSTALL_DIR/rolling-cd-pipeline"
        else
            print_warning "Rolling CD pipeline pack not found in source"
        fi
    fi

    # If the rolling upgrade path pack was requested, stage it in the install tree.
    if [ "$ROLLING_UPGRADE" = true ]; then
        print_info "Staging rolling upgrade path pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-upgrade-path" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-upgrade-path" "$INSTALL_DIR/rolling-upgrade-path" 2>/dev/null || true
            print_success "Rolling upgrade path pack staged at $INSTALL_DIR/rolling-upgrade-path"
        else
            print_warning "Rolling upgrade path pack not found in source"
        fi
    fi

    # If the rolling rollback path pack was requested, stage it in the install tree.
    if [ "$ROLLING_ROLLBACK" = true ]; then
        print_info "Staging rolling rollback path pack..."
        if [ -d "$INSTALL_DIR/packaging/rolling-rollback-path" ]; then
            cp -r "$INSTALL_DIR/packaging/rolling-rollback-path" "$INSTALL_DIR/rolling-rollback-path" 2>/dev/null || true
            print_success "Rolling rollback path pack staged at $INSTALL_DIR/rolling-rollback-path"
        else
            print_warning "Rolling rollback path pack not found in source"
        fi
    fi

    # If the air-gap base chart pack was requested, stage it in the install tree.
    if [ "$AIRGAP_BASE" = true ]; then
        print_info "Staging air-gap base chart pack..."
        if [ -d "$INSTALL_DIR/packaging/airgap" ]; then
            cp -r "$INSTALL_DIR/packaging/airgap" "$INSTALL_DIR/airgap" 2>/dev/null || true
            print_success "Air-gap base chart pack staged at $INSTALL_DIR/airgap"
        else
            print_warning "Air-gap base chart pack not found in source"
        fi
    fi

    # If the air-gap multi-arch build pack was requested, stage it in the install tree.
    if [ "$AIRGAP_MULTI_ARCH" = true ]; then
        print_info "Staging air-gap multi-arch build pack..."
        if [ -d "$INSTALL_DIR/packaging/airgap" ]; then
            cp -r "$INSTALL_DIR/packaging/airgap" "$INSTALL_DIR/airgap" 2>/dev/null || true
            print_success "Air-gap multi-arch build pack staged at $INSTALL_DIR/airgap"
        else
            print_warning "Air-gap multi-arch build pack not found in source"
        fi
    fi

    # If the single-node base chart pack was requested, stage it in the install tree.
    if [ "$SINGLE_NODE_BASE" = true ]; then
        print_info "Staging single-node base chart pack..."
        if [ -d "$INSTALL_DIR/packaging/single-node-base-chart" ]; then
            cp -r "$INSTALL_DIR/packaging/single-node-base-chart" "$INSTALL_DIR/single-node-base-chart" 2>/dev/null || true
            print_success "Single-node base chart pack staged at $INSTALL_DIR/single-node-base-chart"
        else
            print_warning "Single-node base chart pack not found in source"
        fi
    fi

    # If the single-node multi-arch build pack was requested, stage it in the install tree.
    if [ "$SINGLE_NODE_MULTI_ARCH" = true ]; then
        print_info "Staging single-node multi-arch build pack..."
        if [ -d "$INSTALL_DIR/packaging/single-node-multi-arch-build" ]; then
            cp -r "$INSTALL_DIR/packaging/single-node-multi-arch-build" "$INSTALL_DIR/single-node-multi-arch-build" 2>/dev/null || true
            print_success "Single-node multi-arch build pack staged at $INSTALL_DIR/single-node-multi-arch-build"
        else
            print_warning "Single-node multi-arch build pack not found in source"
        fi
    fi

    # If the HA upgrade path pack was requested, stage it in the install tree.
    if [ "$HA_UPGRADE" = true ]; then
        print_info "Staging HA upgrade path pack..."
        if [ -d "$INSTALL_DIR/packaging/ha-upgrade-path" ]; then
            cp -r "$INSTALL_DIR/packaging/ha-upgrade-path" "$INSTALL_DIR/ha-upgrade-path" 2>/dev/null || true
            print_success "HA upgrade path pack staged at $INSTALL_DIR/ha-upgrade-path"
            if [ -d "$INSTALL_DIR/systemd" ]; then
                cp "$INSTALL_DIR/systemd/proxmoxVEx-ha-upgrade.path" "$INSTALL_DIR/" 2>/dev/null || true
                cp "$INSTALL_DIR/systemd/proxmoxVEx-ha-upgrade.service" "$INSTALL_DIR/" 2>/dev/null || true
                print_success "HA upgrade path systemd units staged at $INSTALL_DIR/"
            fi
        else
            print_warning "HA upgrade path pack not found in source"
        fi
    fi

    # If the HA rollback path pack was requested, stage it in the install tree.
    if [ "$HA_ROLLBACK" = true ]; then
        print_info "Staging HA rollback path pack..."
        if [ -d "$INSTALL_DIR/packaging/ha-rollback-path" ]; then
            cp -r "$INSTALL_DIR/packaging/ha-rollback-path" "$INSTALL_DIR/ha-rollback-path" 2>/dev/null || true
            print_success "HA rollback path pack staged at $INSTALL_DIR/ha-rollback-path"
            if [ -d "$INSTALL_DIR/systemd" ]; then
                cp "$INSTALL_DIR/systemd/proxmoxVEx-ha-rollback.path" "$INSTALL_DIR/" 2>/dev/null || true
                cp "$INSTALL_DIR/systemd/proxmoxVEx-ha-rollback.service" "$INSTALL_DIR/" 2>/dev/null || true
                print_success "HA rollback path systemd units staged at $INSTALL_DIR/"
            fi
        else
            print_warning "HA rollback path pack not found in source"
        fi
    fi

    # =========================================================================
    # Step 4: Python Virtual Environment & Dependencies
    # Create an isolated Python environment and install the runtime packages.
    # =========================================================================
    print_step "Step 4/6: Setting up Python Environment"

    # Python version sanity. We test against 3.10–3.13. 3.14 is too new for
    # parts of our stack (gevent, websockets, pyvmomi may have edge cases the
    # ecosystem hasn't worked through yet — issue #388 had a Python 3.14
    # report where the SSH WebSocket subprocess couldn't bind cleanly). 3.9
    # and earlier hit the urllib3/cryptography floor in requirements.txt.
    # PYTHON_BIN may have been chosen by the dependency resolver (custom
    # interpreter path) — the venv below is built with that same interpreter.
    PY_VER=$("$PYTHON_BIN" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
    if [ -z "$PY_VER" ]; then
        echo -e "${RED}$PYTHON_BIN is not callable. Install it first: apt-get install python3 python3-venv${NC}"
        exit 1
    fi
    print_info "Detected Python: $PY_VER ($PYTHON_BIN)"
    PY_MAJOR=$(echo "$PY_VER" | cut -d. -f1)
    PY_MINOR=$(echo "$PY_VER" | cut -d. -f2)
    if [ "$PY_MAJOR" -ne 3 ]; then
        echo -e "${RED}Unsupported Python major version: $PY_VER. ProxmoxVEx requires Python 3.10–3.13.${NC}"
        exit 1
    fi
    if [ "$PY_MINOR" -lt 10 ]; then
        echo -e "${RED}Python $PY_VER is too old. Minimum supported: 3.10. Recommended: 3.12.${NC}"
        echo -e "${RED}Older versions hit the urllib3 / cryptography floors in requirements.txt.${NC}"
        exit 1
    fi
    if [ "$PY_MINOR" -ge 14 ]; then
        echo -e "${YELLOW}WARNING: Python $PY_VER is newer than what ProxmoxVEx is tested on (3.10–3.13).${NC}"
        echo -e "${YELLOW}Known issue on 3.14: SSH/VNC WebSocket subprocesses may fail to bind${NC}"
        echo -e "${YELLOW}cleanly (issue #388). If you hit a console-not-working bug, downgrade to${NC}"
        echo -e "${YELLOW}python3.12 (Ubuntu 24.04 default) or python3.13 and run deploy.sh again.${NC}"
        if [ -t 0 ] && [ -z "$DEPLOY_FORCE_PY" ]; then
            read -r -p "Continue anyway? [y/N] " _ans
            case "$_ans" in
                y|Y|yes|YES) ;;
                *) echo "Aborted. Set DEPLOY_FORCE_PY=1 to skip this prompt in non-interactive runs."; exit 1 ;;
            esac
        else
            print_info "Non-interactive run or DEPLOY_FORCE_PY set — proceeding on $PY_VER."
        fi
    fi

    print_info "Creating virtual environment..."
    "$PYTHON_BIN" -m venv "$INSTALL_DIR/venv"

    print_info "Installing Python packages..."
    "$INSTALL_DIR/venv/bin/pip" install --upgrade pip -q 2>/dev/null

    # Prefer the repository's requirements.txt when available; otherwise fall
    # back to a hard-coded list of known runtime dependencies.
    if [ -f "$INSTALL_DIR/requirements.txt" ]; then
        print_info "Installing from requirements.txt..."
        "$INSTALL_DIR/venv/bin/pip" install -q -r "$INSTALL_DIR/requirements.txt" 2>/dev/null
    else
        # Fallback to hardcoded list
        print_info "No requirements.txt found, using defaults..."
        "$INSTALL_DIR/venv/bin/pip" install -q \
            flask flask-cors flask-sock flask-compress \
            requests urllib3 cryptography pyopenssl \
            argon2-cffi paramiko websockets websocket-client \
            gevent gevent-websocket pyotp "qrcode[pil]" pyvmomi 2>/dev/null
    fi

    print_success "Python environment ready"

    # =========================================================================
    # Step 5: Download Offline Assets (Optional)
    # Pull down CSS/JS/font assets from CDNs so the web UI works without
    # internet after the first install. This is the slowest step, hence the
    # --no-offline / --no-offline flags to skip it when not wanted.
    # =========================================================================
    if [ "$DOWNLOAD_OFFLINE" = true ]; then
        print_step "Step 5/6: Downloading Offline Assets"

        cd "$INSTALL_DIR"
        print_info "Downloading static files for offline mode..."

        if "$INSTALL_DIR/venv/bin/python" "$PYTHON_FILE" --download-static 2>&1 | while read line; do echo -n "."; done; then
            echo ""
            print_success "Offline assets downloaded"
        else
            echo ""
            print_warning "Some assets may have failed (non-critical)"
        fi
    else
        print_step "Step 5/6: Skipping Offline Assets"
        print_info "Use --download-static later if needed"
    fi

    # =========================================================================
    # Step 6: Configure & Start Service
    # Build the systemd unit, helper wrappers, sudoers permissions, and the
    # master key; then start the application and persist the chosen port.
    # =========================================================================
    print_step "Step 6/6: Configuring Service"

    # Ask the operator to choose an access port when running interactively.
    # Otherwise the value from --port or the default (5000) is used.
    if [ "$INTERACTIVE" = true ]; then
        echo -e "${YELLOW}Select access port:${NC}"
        echo "  1) Default (5000) - Standard ports"
        echo "  2) HTTPS (443)    - Professional setup"
        echo "  3) Custom         - Enter your own"
        echo ""

        while true; do
            read -p "Choice [1-3, default=1]: " PORT_CHOICE < /dev/tty
            case "${PORT_CHOICE:-1}" in
                1)
                    ACCESS_PORT=5000
                    break
                    ;;
                2)
                    ACCESS_PORT=443
                    break
                    ;;
                3)
                    read -p "Enter port (1-65535): " CUSTOM_PORT < /dev/tty
                    if [[ "$CUSTOM_PORT" =~ ^[0-9]+$ ]] && [ "$CUSTOM_PORT" -ge 1 ] && [ "$CUSTOM_PORT" -le 65535 ]; then
                        ACCESS_PORT=$CUSTOM_PORT
                        break
                    else
                        echo -e "${RED}Invalid port${NC}"
                    fi
                    ;;
                *)
                    echo "Please enter 1, 2, or 3"
                    ;;
            esac
        done
    fi

    # Announce the selected port and the derived VNC/SSH WebSocket ports.
    # Privileged ports (<1024) are reachable because the unit grants CAP_NET_BIND_SERVICE.
    echo -e "${GREEN}✓ Using ports: $ACCESS_PORT (Web), $((ACCESS_PORT+1)) (VNC), $((ACCESS_PORT+2)) (SSH)${NC}"
    [ "$ACCESS_PORT" -lt 1024 ] && echo -e "${CYAN}  (privileged ports via CAP_NET_BIND_SERVICE)${NC}"

    # Create a systemd service that runs the main Python file as the service user.
    cat > /etc/systemd/system/ProxmoxVEx.service << EOF
[Unit]
Description=ProxmoxVEx - Proxmox Cluster Management
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=$SERVICE_USER
Group=$SERVICE_GROUP
WorkingDirectory=$INSTALL_DIR

# Custom PATH for wrappers
Environment=PATH=$INSTALL_DIR/bin:/usr/local/bin:/usr/bin:/bin

ExecStart=$INSTALL_DIR/venv/bin/python $INSTALL_DIR/$PYTHON_FILE
Restart=always
RestartSec=5

# Allow binding to privileged ports (443, 80)
AmbientCapabilities=CAP_NET_BIND_SERVICE

# Minimal security
PrivateTmp=true

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ProxmoxVEx

[Install]
WantedBy=multi-user.target
EOF

    # Create wrapper scripts for auto-update
    mkdir -p "$INSTALL_DIR/bin"

    # systemctl wrapper
    cat > "$INSTALL_DIR/bin/systemctl" << 'WRAPPEREOF'
#!/bin/bash
# Intelligent systemctl wrapper for ProxmoxVEx auto-update
if [ "$1" = "sudo" ]; then
    shift
fi
case "$*" in
    *ProxmoxVEx*)
        exec /usr/bin/sudo /usr/bin/systemctl "$@"
        ;;
    *)
        exec /usr/bin/systemctl "$@"
        ;;
esac
WRAPPEREOF
    chmod 755 "$INSTALL_DIR/bin/systemctl"

    # sudo wrapper
    cat > "$INSTALL_DIR/bin/sudo" << 'SUDOWRAPPER'
#!/bin/bash
# Sudo wrapper - prevents double sudo
if [ "$1" = "sudo" ]; then
    shift
fi
exec /usr/bin/sudo "$@"
SUDOWRAPPER
    chmod 755 "$INSTALL_DIR/bin/sudo"

    # Create sudoers rules
    cat > /etc/sudoers.d/ProxmoxVEx << EOF
# ProxmoxVEx service management (for auto-update)
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart ProxmoxVEx
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart ProxmoxVEx.service
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop ProxmoxVEx
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop ProxmoxVEx.service
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl start ProxmoxVEx
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl start ProxmoxVEx.service
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl status ProxmoxVEx
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl status ProxmoxVEx.service
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active ProxmoxVEx
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active ProxmoxVEx.service
EOF
    chmod 440 /etc/sudoers.d/ProxmoxVEx

    # Set ownership of the install tree to the service account.
    chown -R "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR"

    # Harden sensitive dirs
    [ -d "$INSTALL_DIR/config" ] && chmod 700 "$INSTALL_DIR/config"
    [ -d "$INSTALL_DIR/ssl" ] && chmod 700 "$INSTALL_DIR/ssl"

    # MK May 2026 — master-key bootstrap (Tier-4: /etc/ProxmoxVEx/secret.key).
    # Fresh installs get the key OUTSIDE $INSTALL_DIR/config so a backup of the
    # config dir doesn't pick up the decryption key. Idempotent: only acts when
    # no key already exists (neither at the new location nor in the legacy spot).
    #
    # MK May 2026 (#417 / tgmct) — mode is 0640 (NOT 0600). File is owned by
    # root:$SERVICE_GROUP; the systemd unit runs as $SERVICE_USER which is in
    # $SERVICE_GROUP, so group-read is required to load the key at boot.
    # The previous 0600 root:ProxmoxVEx combo made the key unreadable to the
    # service and ProxmoxVEx.service failed to start on every fresh install.
    LEGACY_KEY="$INSTALL_DIR/config/.ProxmoxVEx.key"
    SYS_KEY_DIR="/etc/ProxmoxVEx"
    SYS_KEY="$SYS_KEY_DIR/secret.key"

    if [ -f "$SYS_KEY" ]; then
        # Repair-on-upgrade: prior deploy.sh versions wrote 0600. Bump to 0640
        # so the systemd service can actually read its own key after upgrade.
        cur_mode=$(stat -c '%a' "$SYS_KEY" 2>/dev/null || echo "")
        if [ "$cur_mode" = "600" ] || [ "$cur_mode" = "400" ]; then
            print_info "Found $SYS_KEY at mode $cur_mode — bumping to 0640 (#417 repair)"
            chmod 640 "$SYS_KEY"
            chown "root:$SERVICE_GROUP" "$SYS_KEY" 2>/dev/null || true
        else
            print_info "Master key already at $SYS_KEY (mode $cur_mode) — leaving untouched"
        fi
    elif [ -f "$LEGACY_KEY" ]; then
        print_warning "Legacy key at $LEGACY_KEY detected"
        print_info "  ProxmoxVEx will keep using it but emit a deprecation warning."
        print_info "  Migrate with:  sudo mv \"$LEGACY_KEY\" \"$SYS_KEY\" && sudo chmod 640 \"$SYS_KEY\" && sudo chown root:$SERVICE_GROUP \"$SYS_KEY\""
    else
        # No key anywhere — generate the new default at the secure location.
        mkdir -p "$SYS_KEY_DIR"
        chmod 750 "$SYS_KEY_DIR"
        chown "root:$SERVICE_GROUP" "$SYS_KEY_DIR" 2>/dev/null || true

        # 32 raw bytes -> urlsafe-base64. Python is already a hard dep at this
        # point in the install so we don't need a bash-only fallback.
        if "$INSTALL_DIR/venv/bin/python3" -c "
import base64, os, secrets, sys
key = base64.urlsafe_b64encode(secrets.token_bytes(32))
fd = os.open('$SYS_KEY', os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640)
try:
    os.write(fd, key)
finally:
    os.close(fd)
" 2>/dev/null; then
            chmod 640 "$SYS_KEY"
            chown "root:$SERVICE_GROUP" "$SYS_KEY" 2>/dev/null || \
                chown "root:root" "$SYS_KEY"
            print_success "Generated master key at $SYS_KEY (0640 root:$SERVICE_GROUP)"
            print_info "  Loader tier: 4 (system-service default — outside $INSTALL_DIR/config)"
            print_info "  Stronger: wrap with systemd-creds — see docs/SECURITY.md §5"
        else
            print_warning "Could not pre-generate $SYS_KEY — ProxmoxVEx will fall back to legacy path on first boot"
        fi
    fi

    # Enable and start service
    systemctl daemon-reload
    systemctl enable ProxmoxVEx
    systemctl start ProxmoxVEx

    print_success "Systemd service created and started"

    # Wait for database initialization
    echo "Waiting for database initialization..."
    sleep 8

    # If a non-default port was chosen, write it to the SQLite settings table
    # and restart the service so it takes effect on this first run.
    if [ "$ACCESS_PORT" != 5000 ]; then
        print_info "Configuring port $ACCESS_PORT..."
        ProxmoxVEx_DB="$INSTALL_DIR/config/ProxmoxVEx.db"

        if [ -f "$ProxmoxVEx_DB" ]; then
            sqlite3 "$ProxmoxVEx_DB" "INSERT OR REPLACE INTO server_settings (key, value) VALUES ('port', '$ACCESS_PORT');" 2>/dev/null && {
                echo "Restarting with new port..."
                systemctl restart ProxmoxVEx
                sleep 5
                print_success "Port set to $ACCESS_PORT"
            } || print_warning "Set port manually in Settings > Server"
        fi
    fi

    # Confirm the service is now active.
    if systemctl is-active --quiet ProxmoxVEx; then
        print_success "ProxmoxVEx is running!"
    else
        print_error "ProxmoxVEx failed to start - check: journalctl -u ProxmoxVEx"
    fi

    # 984-single-node-template-pack: Stage the lightweight single-node pack.
    apply_single_node_template_pack

    # 985-single-node-replicated-service: Stage the replicated service manifest.
    apply_single_node_replicated_service

    # 986-single-node-signed-images: Stage the signed image manifest.
    apply_single_node_signed_images

    # 987-single-node-vulnerability-scan: Stage the vulnerability scan manifest.
    apply_single_node_vulnerability_scan

    # 988-single-node-ci-pipeline: Stage the CI pipeline manifest.
    apply_single_node_ci_pipeline

    # 989-single-node-cd-pipeline: Stage the CD pipeline manifest.
    apply_single_node_cd_pipeline

    # 990-single-node-upgrade-path: Stage the upgrade path manifest.
    apply_single_node_upgrade_path

    # 991-single-node-rollback-path: Stage the rollback path manifest.
    apply_single_node_rollback_path

    # 992-ha-base-chart: Stage the HA base chart manifest.
    apply_ha_base_chart

    # 993-ha-multi-arch-build: Stage the HA multi-arch build manifest.
    apply_ha_multi_arch_build

    # 994-ha-template-pack: Stage the HA template pack manifest.
    apply_ha_template_pack

    # 995-ha-replicated-service: Stage the HA replicated service manifest.
    apply_ha_replicated_service

    # 996-ha-signed-images: Stage the HA signed images manifest.
    apply_ha_signed_images

    # 997-ha-vulnerability-scan: Stage the HA vulnerability scan manifest.
    apply_ha_vulnerability_scan

    # 998-ha-ci-pipeline: Stage the HA CI pipeline manifest.
    apply_ha_ci_pipeline

    # 999-ha-cd-pipeline: Stage the HA CD pipeline manifest.
    apply_ha_cd_pipeline

}

# =============================================================================
# Final Summary
# Plain-text completion banner + access URLs. Used directly in classic mode
# and echoed after the closing TUI screen so the details survive in scrollback.
# =============================================================================
print_final_summary() {
    echo ""
    echo -e "${GREEN}╔════════════════════════════════════════════════════════════════════════════╗${NC}"
    echo -e "${GREEN}║                    Installation Complete! 🎉                               ║${NC}"
    echo -e "${GREEN}╚════════════════════════════════════════════════════════════════════════════╝${NC}"
    echo ""

    # Get current IP
    CURRENT_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
    [ -z "$CURRENT_IP" ] && CURRENT_IP="<your-ip>"

    if [ "$ACCESS_PORT" = 443 ]; then
        echo -e "  Web Interface: ${CYAN}${BOLD}https://${CURRENT_IP}${NC}"
        echo -e "  VNC WebSocket: ${CYAN}https://${CURRENT_IP}:444${NC}"
        echo -e "  SSH WebSocket: ${CYAN}https://${CURRENT_IP}:445${NC}"
    else
        echo -e "  Web Interface: ${CYAN}${BOLD}https://${CURRENT_IP}:${ACCESS_PORT}${NC}"
        echo -e "  VNC WebSocket: ${CYAN}https://${CURRENT_IP}:$((ACCESS_PORT+1))${NC}"
        echo -e "  SSH WebSocket: ${CYAN}https://${CURRENT_IP}:$((ACCESS_PORT+2))${NC}"
    fi

    echo ""
    echo -e "${YELLOW}💡 Tip: Check for updates in ProxmoxVEx Web UI${NC}"
    echo -e "   Settings → Updates → Check for Updates"
    echo ""
    echo -e "Commands:"
    echo -e "  ${CYAN}systemctl status ProxmoxVEx${NC}    - Check status"
    echo -e "  ${CYAN}journalctl -u ProxmoxVEx -f${NC}    - View logs"
    echo -e "  ${CYAN}systemctl restart ProxmoxVEx${NC}   - Restart service"
    echo ""
}

# =============================================================================
# Entry Point
# Preflight (root + connectivity) runs before the TUI is bootstrapped: both
# are prerequisites for apt-installing `dialog`, and their errors are clearer
# as plain lines than as a curses screen that flashes past.
# =============================================================================
main() {
    # The installer needs root to install packages, create users, and write
    # systemd units and /etc paths. Bail early if not root.
    if [ "$EUID" -ne 0 ]; then
        print_error "Please run as root: sudo $0"
        exit 1
    fi

    # Curl-pipe installs need outbound connectivity to fetch the release
    # archive from the main site. curl-first probing (with ping fallback)
    # avoids false "offline" verdicts on hosts where ICMP is filtered or
    # iputils-ping simply isn't installed yet.
    # `|| net_rc=$?` guards against `set -e` killing the script before the
    # case can read the probe's return code.
    net_rc=0
    check_connectivity || net_rc=$?
    case "$net_rc" in
        0) ;;
        1)
            print_error "No internet connection. Cannot download ProxmoxVEx."
            exit 1
            ;;
        2)
            print_warning "Cannot verify connectivity (no curl/ping yet) — continuing anyway"
            ;;
    esac

    # Decide between the full-screen dialog UI and classic line output.
    setup_tui

    # Scan prerequisites once so both UIs report from the same data — the TUI
    # renders it as a status screen, classic mode as status lines.
    dep_collect_missing
    dep_python_state
    dep_disk_check

    if [ "$TUI" = true ]; then
        tui_main
    else
        print_banner
        dep_classic_report
        run_install
        print_final_summary
    fi
}

# 984-single-node-template-pack: Apply a minimal single-node template pack.
# This helper stages the lightweight templates used for single-node deployments.
apply_single_node_template_pack() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-template-pack"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/template-pack.json" << 'EOF'
{
  "name": "single-node-template-pack",
  "version": "1.2.361",
  "description": "Lightweight single-node ProxmoxVEx deployment template pack",
  "templates": [
    {
      "name": "single-node-default",
      "description": "Default single-node ProxmoxVEx deployment template",
      "compose": "docker-compose.yml"
    }
  ]
}
EOF
    echo -e "${GREEN}✓ Single-Node Template Pack ready at ${pack_dir}/template-pack.json${NC}"
}

# 985-single-node-replicated-service: Apply a replicated service manifest.
# This helper stages the single-node replicated service configuration.
apply_single_node_replicated_service() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-replicated-service"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/replicated-service.json" << 'EOF'
{
  "name": "single-node-replicated-service",
  "version": "1.2.362",
  "description": "Single-node ProxmoxVEx replicated service configuration",
  "replicas": 2,
  "service": "ProxmoxVEx"
}
EOF
    echo -e "${GREEN}✓ Single-Node Replicated Service ready at ${pack_dir}/replicated-service.json${NC}"
}

# 986-single-node-signed-images: Apply a signed image manifest.
# This helper stages the single-node signed image metadata.
apply_single_node_signed_images() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-signed-images"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/signed-images.json" << 'EOF'
{
  "name": "single-node-signed-images",
  "version": "1.2.363",
  "description": "Single-node ProxmoxVEx signed image configuration",
  "image": "ghcr.io/armatec/proxmoxvex",
  "signed": true
}
EOF
    echo -e "${GREEN}✓ Single-Node Signed Images ready at ${pack_dir}/signed-images.json${NC}"
}

# 987-single-node-vulnerability-scan: Apply a vulnerability scan manifest.
# This helper stages the single-node vulnerability scan metadata.
apply_single_node_vulnerability_scan() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-vulnerability-scan"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/vulnerability-scan.json" << 'EOF'
{
  "name": "single-node-vulnerability-scan",
  "version": "1.2.364",
  "description": "Single-node ProxmoxVEx vulnerability scan configuration",
  "enabled": true,
  "scanner": "trivy",
  "severity_threshold": "HIGH"
}
EOF
    echo -e "${GREEN}✓ Single-Node Vulnerability Scan ready at ${pack_dir}/vulnerability-scan.json${NC}"
}

# 988-single-node-ci-pipeline: Apply a CI pipeline manifest.
# This helper stages the single-node CI pipeline metadata.
apply_single_node_ci_pipeline() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-ci-pipeline"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/ci-pipeline.json" << 'EOF'
{
  "name": "single-node-ci-pipeline",
  "version": "1.2.365",
  "description": "Single-node ProxmoxVEx CI pipeline configuration",
  "stages": ["lint", "test", "build", "package"]
}
EOF
    echo -e "${GREEN}✓ Single-Node CI Pipeline ready at ${pack_dir}/ci-pipeline.json${NC}"
}

# 989-single-node-cd-pipeline: Apply a CD pipeline manifest.
# This helper stages the single-node CD pipeline metadata.
apply_single_node_cd_pipeline() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-cd-pipeline"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/cd-pipeline.json" << 'EOF'
{
  "name": "single-node-cd-pipeline",
  "version": "1.2.366",
  "description": "Single-node ProxmoxVEx CD pipeline configuration",
  "stages": ["deploy", "verify", "promote"]
}
EOF
    echo -e "${GREEN}✓ Single-Node CD Pipeline ready at ${pack_dir}/cd-pipeline.json${NC}"
}

# 990-single-node-upgrade-path: Apply an upgrade path manifest.
# This helper stages the single-node upgrade path metadata.
apply_single_node_upgrade_path() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-upgrade-path"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/upgrade-path.json" << 'EOF'
{
  "name": "single-node-upgrade-path",
  "version": "1.2.367",
  "description": "Single-node ProxmoxVEx upgrade path configuration",
  "steps": ["backup", "deploy-canary", "health-check", "promote"]
}
EOF
    echo -e "${GREEN}✓ Single-Node Upgrade Path ready at ${pack_dir}/upgrade-path.json${NC}"
}

# 991-single-node-rollback-path: Apply a rollback path manifest.
# This helper stages the single-node rollback path metadata.
apply_single_node_rollback_path() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/single-node-rollback-path"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/rollback-path.json" << 'EOF'
{
  "name": "single-node-rollback-path",
  "version": "1.2.368",
  "description": "Single-node ProxmoxVEx rollback path configuration",
  "steps": ["stop-traffic", "restore-stable", "redeploy", "verify"]
}
EOF
    echo -e "${GREEN}✓ Single-Node Rollback Path ready at ${pack_dir}/rollback-path.json${NC}"
}

# 992-ha-base-chart: Apply an HA base chart manifest.
# This helper stages the HA base chart metadata.
apply_ha_base_chart() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-base-chart"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/base-chart.json" << 'EOF'
{
  "name": "ha-base-chart",
  "version": "1.2.369",
  "description": "HA ProxmoxVEx base chart configuration",
  "replicas": 3,
  "mode": "ha"
}
EOF
    echo -e "${GREEN}✓ HA Base Chart ready at ${pack_dir}/base-chart.json${NC}"
}

# 993-ha-multi-arch-build: Apply an HA multi-arch build manifest.
# This helper stages the HA multi-arch build metadata.
apply_ha_multi_arch_build() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-multi-arch-build"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/multi-arch-build.json" << 'EOF'
{
  "name": "ha-multi-arch-build",
  "version": "1.2.370",
  "description": "HA ProxmoxVEx multi-arch build configuration",
  "platforms": ["linux/amd64", "linux/arm64"],
  "mode": "ha"
}
EOF
    echo -e "${GREEN}✓ HA Multi-Arch Build ready at ${pack_dir}/multi-arch-build.json${NC}"
}

# 994-ha-template-pack: Apply an HA template pack manifest.
# This helper stages the HA template pack metadata.
apply_ha_template_pack() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-template-pack"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/template-pack.json" << 'EOF'
{
  "name": "ha-template-pack",
  "version": "1.2.371",
  "description": "HA ProxmoxVEx template pack configuration",
  "templates": ["base", "replicated", "signed"],
  "mode": "ha"
}
EOF
    echo -e "${GREEN}✓ HA Template Pack ready at ${pack_dir}/template-pack.json${NC}"
}

# 995-ha-replicated-service: Apply an HA replicated service manifest.
# This helper stages the HA replicated service metadata.
apply_ha_replicated_service() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-replicated-service"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/replicated-service.json" << 'EOF'
{
  "name": "ha-replicated-service",
  "version": "1.2.373",
  "description": "HA ProxmoxVEx replicated service configuration",
  "replicas": 3,
  "mode": "ha",
  "quorum": true
}
EOF
    echo -e "${GREEN}✓ HA Replicated Service ready at ${pack_dir}/replicated-service.json${NC}"
}

# 996-ha-signed-images: Apply an HA signed images manifest.
# This helper stages the HA signed images metadata.
apply_ha_signed_images() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-signed-images"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/signed-images.json" << 'EOF'
{
  "name": "ha-signed-images",
  "version": "1.2.374",
  "description": "HA ProxmoxVEx signed image configuration",
  "signer": "cosign",
  "verify": true,
  "mode": "ha"
}
EOF
    echo -e "${GREEN}✓ HA Signed Images ready at ${pack_dir}/signed-images.json${NC}"
}

# 997-ha-vulnerability-scan: Apply an HA vulnerability scan manifest.
# This helper stages the HA vulnerability scan metadata.
apply_ha_vulnerability_scan() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-vulnerability-scan"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/vulnerability-scan.json" << 'EOF'
{
  "name": "ha-vulnerability-scan",
  "version": "1.2.375",
  "description": "HA ProxmoxVEx vulnerability scan configuration",
  "scanner": "trivy",
  "severity_threshold": "high",
  "mode": "ha"
}
EOF
    echo -e "${GREEN}✓ HA Vulnerability Scan ready at ${pack_dir}/vulnerability-scan.json${NC}"
}

# 998-ha-ci-pipeline: Apply an HA CI pipeline manifest.
# This helper stages the HA CI pipeline metadata.
apply_ha_ci_pipeline() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-ci-pipeline"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/ci-pipeline.json" << 'EOF'
{
  "name": "ha-ci-pipeline",
  "version": "1.2.376",
  "description": "HA ProxmoxVEx CI pipeline configuration",
  "steps": ["lint", "test", "build", "sign"],
  "mode": "ha"
}
EOF
    echo -e "${GREEN}✓ HA CI Pipeline ready at ${pack_dir}/ci-pipeline.json${NC}"
}

# 999-ha-cd-pipeline: Apply an HA CD pipeline manifest.
# This helper stages the HA CD pipeline metadata.
apply_ha_cd_pipeline() {
    local pack_dir="${SCRIPT_DIR:-.}/packaging/ha-cd-pipeline"
    mkdir -p "${pack_dir}"
    cat > "${pack_dir}/cd-pipeline.json" << 'EOF'
{
  "name": "ha-cd-pipeline",
  "version": "1.2.377",
  "description": "HA ProxmoxVEx CD pipeline configuration",
  "steps": ["stage", "deploy-canary", "health-check", "promote"],
  "mode": "ha"
}
EOF
    echo -e "${GREEN}✓ HA CD Pipeline ready at ${pack_dir}/cd-pipeline.json${NC}"
}

# Kick off the install routine, passing through any command-line arguments.
main "$@"
