#!/usr/bin/env bash
# proxmox-setup - Interactive Proxmox post-install configuration for Hetzner
#
# Configures a fresh Proxmox host with private networking, ZFS storage,
# WireGuard tunnel, firewall lockdown, and API tokens for incusos-proxmox.
#
# Runs from your workstation over SSH. Requires an SSH host alias configured
# in ~/.ssh/config (e.g. "hetzner-lab").
#
# The script first detects the full state of the host, shows a status
# dashboard, and then only offers to change what's actually needed.
#
# Usage: proxmox-setup --host <ssh-alias> [OPTIONS]
# Run 'proxmox-setup --help' for full usage information.

set -euo pipefail

readonly VERSION="0.2.0"
readonly SCRIPT_NAME="proxmox-setup"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ---------------------------------------------------------------------------
# Defaults
# ---------------------------------------------------------------------------

SSH_HOST=""
DRY_RUN=false
VERBOSE=false
QUIET=false
YES=false
SUBNET="10.10.0.0/24"
WG_SUBNET="10.10.99.0/24"
WG_PORT="51820"
SKIP_REPOS=false
SKIP_WIREGUARD=false
SKIP_FIREWALL=false
SKIP_STORAGE=false

# Derived (computed after argument parsing)
SUBNET_BASE=""          # e.g. 10.10.0
SUBNET_MASK=""          # e.g. 24
SUBNET_GW=""            # e.g. 10.10.0.1
WG_SUBNET_BASE=""       # e.g. 10.10.99
WG_SUBNET_MASK=""       # e.g. 24
WG_SERVER_IP=""         # e.g. 10.10.99.1
WG_CLIENT_IP=""         # e.g. 10.10.99.2

# ---------------------------------------------------------------------------
# Detected state (populated by detect_all)
# ---------------------------------------------------------------------------

# Host basics
DETECT_HOSTNAME=""
DETECT_KERNEL=""
DETECT_PVE_VERSION=""
DETECT_PUBLIC_IP=""
DETECT_BLOCK_DEVICES=""

# Repos
STATE_REPOS="needed"            # done | needed | partial
DETECT_ENTERPRISE_ACTIVE=""     # non-empty if enterprise repos active
DETECT_NOSUB_EXISTS=""          # "yes" if no-sub repo file exists
DETECT_NAG_PATCHED=""           # "yes" if nag popup already patched

# Updates
STATE_UPDATE="unknown"          # done | needed | unknown
DETECT_UPGRADABLE_COUNT="0"

# Bridge
STATE_BRIDGE="needed"           # done | needed
DETECT_VMBR1_IP=""              # e.g. "10.10.0.1/24" or empty
DETECT_IP_FORWARD=""            # "1" or "0"
DETECT_NAT_ACTIVE=""            # "yes" or empty

# Storage
STATE_STORAGE="done"            # done | available | needed
DETECT_ZPOOLS=""
DETECT_PVE_STORAGE=""
DETECT_AVAILABLE_DISKS=""
DETECT_AVAILABLE_DISK_COUNT="0"

# WireGuard
STATE_WIREGUARD="needed"        # done | needed
DETECT_WG_INSTALLED=""          # "yes" or empty
DETECT_WG_ACTIVE=""             # "yes" or empty
DETECT_WG_PEERS=""              # peer count
DETECT_WG_HANDSHAKE=""          # latest handshake info

# Firewall
STATE_FIREWALL="needed"         # done | needed
DETECT_NFT_RULES=""             # "yes" if nftables rules loaded with our pattern
DETECT_NFT_FILE=""              # "yes" if config file exists

# API token
STATE_API="needed"              # done | partial | needed
DETECT_API_ROLE=""              # "yes" or empty
DETECT_API_POOL=""              # "yes" or empty
DETECT_API_TOKEN=""             # "yes" or empty

# ---------------------------------------------------------------------------
# Color and formatting
# ---------------------------------------------------------------------------

setup_colors() {
    if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        CYAN='\033[0;36m'
        BOLD='\033[1m'
        DIM='\033[2m'
        RESET='\033[0m'
    else
        RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
    fi
}

info()    { [[ "$QUIET" == true ]] && return; echo -e "${BLUE}[info]${RESET}    $*"; }
success() { [[ "$QUIET" == true ]] && return; echo -e "${GREEN}[ok]${RESET}      $*"; }
warn()    { echo -e "${YELLOW}[warn]${RESET}    $*" >&2; }
error()   { echo -e "${RED}[error]${RESET}   $*" >&2; }
step()    { [[ "$QUIET" == true ]] && return; echo -e "${CYAN}[step]${RESET}    ${BOLD}$*${RESET}"; }
detail()  { [[ "$VERBOSE" != true ]] && return; echo -e "${DIM}          $*${RESET}"; }

dry_run_guard() {
    if [[ "$DRY_RUN" == true ]]; then
        info "(dry-run) $*"
        return 0
    fi
    return 1
}

# Status indicator for the dashboard
status_icon() {
    local state="$1"
    case "$state" in
        done)     echo -e "${GREEN}done${RESET}" ;;
        needed)   echo -e "${YELLOW}needed${RESET}" ;;
        partial)  echo -e "${YELLOW}partial${RESET}" ;;
        available) echo -e "${BLUE}available${RESET}" ;;
        skipped)  echo -e "${DIM}skipped${RESET}" ;;
        unknown)  echo -e "${DIM}unknown${RESET}" ;;
    esac
}

# ---------------------------------------------------------------------------
# SSH helpers
# ---------------------------------------------------------------------------

remote() {
    ssh -o ConnectTimeout=10 -o BatchMode=yes "$SSH_HOST" "$@"
}

remote_output() {
    ssh -o ConnectTimeout=10 -o BatchMode=yes "$SSH_HOST" "$@" 2>/dev/null
}

remote_write() {
    local dest="$1"
    local content="$2"
    ssh -o ConnectTimeout=10 -o BatchMode=yes "$SSH_HOST" "cat > '$dest'" <<< "$content"
}

# ---------------------------------------------------------------------------
# Interactive helpers
# ---------------------------------------------------------------------------

confirm() {
    local prompt="$1"
    if [[ "$YES" == true ]]; then
        return 0
    fi
    echo -en "${BOLD}${prompt}${RESET} [y/N] "
    read -r answer
    [[ "$answer" =~ ^[Yy]$ ]]
}

prompt_value() {
    local prompt="$1"
    local default="$2"
    local result
    if [[ "$YES" == true ]]; then
        echo "$default"
        return
    fi
    echo -en "${BOLD}${prompt}${RESET} [${default}]: "
    read -r result
    echo "${result:-$default}"
}

# ---------------------------------------------------------------------------
# Help
# ---------------------------------------------------------------------------

usage() {
    cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Interactive Proxmox post-install configuration for Hetzner

${BOLD}USAGE${RESET}
    ${SCRIPT_NAME} --host <ssh-alias> [OPTIONS]

${BOLD}DESCRIPTION${RESET}
    Detects the full state of a Proxmox host, shows a status dashboard,
    and interactively configures what's needed:

    - No-subscription repositories and system update
    - Private bridge (vmbr1) with NAT masquerading
    - ZFS storage pool on extra disks
    - WireGuard tunnel for secure remote access
    - nftables firewall lockdown
    - API token for incusos-proxmox automation

    Already-configured items are detected and skipped automatically.
    Each pending step prompts for confirmation. Use --yes to skip prompts.

${BOLD}OPTIONS${RESET}
    ${BOLD}--host ALIAS${RESET}          SSH host alias (required, e.g. hetzner-lab)
    ${BOLD}--subnet CIDR${RESET}         Private bridge subnet (default: ${SUBNET})
    ${BOLD}--wg-subnet CIDR${RESET}      WireGuard subnet (default: ${WG_SUBNET})
    ${BOLD}--wg-port PORT${RESET}        WireGuard listen port (default: ${WG_PORT})
    ${BOLD}--skip-repos${RESET}          Skip repository changes
    ${BOLD}--skip-storage${RESET}        Skip storage pool creation
    ${BOLD}--skip-wireguard${RESET}      Skip WireGuard setup
    ${BOLD}--skip-firewall${RESET}       Skip firewall lockdown
    ${BOLD}-n, --dry-run${RESET}         Preview actions without executing
    ${BOLD}-y, --yes${RESET}             Skip confirmations (use defaults)
    ${BOLD}-v, --verbose${RESET}         Show detailed output
    ${BOLD}-q, --quiet${RESET}           Suppress informational output
    ${BOLD}-h, --help${RESET}            Show this help message
    ${BOLD}-V, --version${RESET}         Show version

${BOLD}EXAMPLES${RESET}
    # See what needs doing (safe, read-only)
    ${SCRIPT_NAME} --host hetzner-lab --dry-run

    # Full setup with defaults
    ${SCRIPT_NAME} --host hetzner-lab --yes

    # Custom subnets, skip firewall
    ${SCRIPT_NAME} --host hetzner-lab --subnet 172.16.0.0/24 --skip-firewall

${BOLD}PREREQUISITES${RESET}
    - SSH access to the Proxmox host (key-based auth recommended)
    - Add an entry to ~/.ssh/config:
        Host hetzner-lab
            HostName <public-ip>
            User root
            IdentityFile ~/.ssh/id_ed25519

${BOLD}SEE ALSO${RESET}
    hetzner/hetzner-setup.md    Manual installation guide
    incusos/targets/hetzner/    Target config files
EOF
    exit 0
}

# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --host)
                SSH_HOST="${2:?'--host requires an SSH alias'}"
                shift 2
                ;;
            --subnet)
                SUBNET="${2:?'--subnet requires a CIDR'}"
                shift 2
                ;;
            --wg-subnet)
                WG_SUBNET="${2:?'--wg-subnet requires a CIDR'}"
                shift 2
                ;;
            --wg-port)
                WG_PORT="${2:?'--wg-port requires a port number'}"
                shift 2
                ;;
            --skip-repos)
                SKIP_REPOS=true
                shift
                ;;
            --skip-storage)
                SKIP_STORAGE=true
                shift
                ;;
            --skip-wireguard)
                SKIP_WIREGUARD=true
                shift
                ;;
            --skip-firewall)
                SKIP_FIREWALL=true
                shift
                ;;
            -n|--dry-run)
                DRY_RUN=true
                shift
                ;;
            -y|--yes)
                YES=true
                shift
                ;;
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -q|--quiet)
                QUIET=true
                shift
                ;;
            -h|--help)
                usage
                ;;
            -V|--version)
                echo "${SCRIPT_NAME} v${VERSION}"
                exit 0
                ;;
            -*)
                error "Unknown option: $1"
                echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
                exit 1
                ;;
            *)
                error "Unexpected argument: $1"
                echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
                exit 1
                ;;
        esac
    done

    if [[ -z "$SSH_HOST" ]]; then
        error "--host is required (SSH alias, e.g. hetzner-lab)"
        echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
        exit 1
    fi
}

# ---------------------------------------------------------------------------
# Subnet helpers
# ---------------------------------------------------------------------------

parse_subnets() {
    SUBNET_MASK="${SUBNET##*/}"
    local subnet_network="${SUBNET%%/*}"
    SUBNET_BASE="${subnet_network%.*}"
    SUBNET_GW="${SUBNET_BASE}.1"

    WG_SUBNET_MASK="${WG_SUBNET##*/}"
    local wg_network="${WG_SUBNET%%/*}"
    WG_SUBNET_BASE="${wg_network%.*}"
    WG_SERVER_IP="${WG_SUBNET_BASE}.1"
    WG_CLIENT_IP="${WG_SUBNET_BASE}.2"
}

# ---------------------------------------------------------------------------
# Detection: gather all state in one pass
# ---------------------------------------------------------------------------

detect_all() {
    step "Detecting host state"

    # Test SSH connectivity
    if ! remote "true" 2>/dev/null; then
        error "Cannot connect to '${SSH_HOST}' via SSH"
        error "Ensure the host is in ~/.ssh/config and key auth works:"
        echo "  ssh ${SSH_HOST} hostname" >&2
        exit 1
    fi
    success "SSH connection to ${SSH_HOST}"

    # Gather everything in a single SSH call for speed.
    # Uses a quoted heredoc (<<'DETECT') so nothing is expanded locally --
    # all $, ", etc. are passed literally to the remote shell.
    local detection_blob
    detection_blob=$(ssh -o ConnectTimeout=10 -o BatchMode=yes "$SSH_HOST" bash <<'DETECT' 2>/dev/null || true
echo '---HOSTNAME---'
hostname
echo '---KERNEL---'
uname -r
echo '---PVE_VERSION---'
pvesh get /version --output-format json 2>/dev/null \
    | python3 -c 'import sys,json; print(json.load(sys.stdin)["version"])' 2>/dev/null \
    || echo 'unknown'
echo '---PUBLIC_IP---'
hostname -I | awk '{print $1}'
echo '---BLOCK_DEVICES---'
lsblk -d -o NAME,SIZE,MODEL,ROTA,TYPE 2>/dev/null
echo '---ZPOOLS---'
zpool list -H 2>/dev/null || true
echo '---PVE_STORAGE---'
pvesm status 2>/dev/null || true
echo '---ENTERPRISE_REPOS---'
grep -rl '^deb.*enterprise' /etc/apt/sources.list.d/*.list 2>/dev/null || echo 'none'
echo '---NOSUB_REPO---'
test -f /etc/apt/sources.list.d/pve-no-subscription.list && echo 'yes' || echo 'no'
echo '---NAG_PATCHED---'
grep -q 'void({' /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2>/dev/null && echo 'yes' || echo 'no'
echo '---UPGRADABLE---'
apt list --upgradable 2>/dev/null | grep -c upgradable || true
echo '---VMBR1---'
ip -br addr show vmbr1 2>/dev/null || echo 'none'
echo '---IP_FORWARD---'
cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo '0'
echo '---NAT_ACTIVE---'
iptables -t nat -L POSTROUTING -n 2>/dev/null | grep -q MASQUERADE && echo 'yes' || echo 'no'
echo '---WG_INSTALLED---'
command -v wg >/dev/null 2>&1 && echo 'yes' || echo 'no'
echo '---WG_ACTIVE---'
wg show wg0 2>/dev/null | head -1 || echo 'none'
echo '---WG_PEERS---'
wg show wg0 peers 2>/dev/null | wc -l || echo '0'
echo '---WG_HANDSHAKE---'
wg show wg0 latest-handshakes 2>/dev/null | head -1 || echo 'none'
echo '---NFT_FILE---'
test -f /etc/nftables-hetzner.conf && echo 'yes' || echo 'no'
echo '---NFT_RULES---'
nft list ruleset 2>/dev/null | grep -q 'nft-drop' && echo 'yes' || echo 'no'
echo '---API_ROLE---'
pveum role list --output-format json 2>/dev/null \
    | python3 -c 'import sys,json; print("yes" if any(r["roleid"]=="IncusOSDeployer" for r in json.load(sys.stdin)) else "no")' 2>/dev/null \
    || echo 'no'
echo '---API_POOL---'
pveum pool list --output-format json 2>/dev/null \
    | python3 -c 'import sys,json; print("yes" if any(p["poolid"]=="IncusLab" for p in json.load(sys.stdin)) else "no")' 2>/dev/null \
    || echo 'no'
echo '---API_TOKEN---'
pveum user token list automation@pve --output-format json 2>/dev/null \
    | python3 -c 'import sys,json; print("yes" if any(t["tokenid"]=="deploy" for t in json.load(sys.stdin)) else "no")' 2>/dev/null \
    || echo 'no'
echo '---AVAILABLE_DISKS---'
used_disks=$(zpool status 2>/dev/null | grep -oP '/dev/\S+' | sed 's|/dev/||' || true)
lsblk -dno NAME,SIZE,TYPE | while read name size type; do
    [ "$type" != 'disk' ] && continue
    echo "$used_disks" | grep -qw "$name" && continue
    lsblk -no MOUNTPOINT /dev/$name 2>/dev/null | grep -q '/' && continue
    echo "$name $size"
done
echo '---END---'
DETECT
)

    # Parse the blob into variables
    local section=""
    local section_lines=""
    while IFS= read -r line; do
        if [[ "$line" == ---*--- ]]; then
            # Process previous section
            if [[ -n "$section" ]]; then
                _parse_section "$section" "$section_lines"
            fi
            section="${line//---/}"
            section_lines=""
        else
            if [[ -n "$section_lines" ]]; then
                section_lines="${section_lines}
${line}"
            else
                section_lines="$line"
            fi
        fi
    done <<< "$detection_blob"
    # Process last section
    if [[ -n "$section" ]]; then _parse_section "$section" "$section_lines"; fi

    # Determine composite states
    _compute_states
}

_parse_section() {
    local section="$1"
    local content="$2"
    content="${content#$'\n'}"  # strip leading newline

    case "$section" in
        HOSTNAME)       DETECT_HOSTNAME="$content" ;;
        KERNEL)         DETECT_KERNEL="$content" ;;
        PVE_VERSION)    DETECT_PVE_VERSION="$content" ;;
        PUBLIC_IP)      DETECT_PUBLIC_IP="$content" ;;
        BLOCK_DEVICES)  DETECT_BLOCK_DEVICES="$content" ;;
        ZPOOLS)         DETECT_ZPOOLS="$content" ;;
        PVE_STORAGE)    DETECT_PVE_STORAGE="$content" ;;
        ENTERPRISE_REPOS)
            if [[ "$content" == "none" ]]; then
                DETECT_ENTERPRISE_ACTIVE=""
            else
                DETECT_ENTERPRISE_ACTIVE="$content"
            fi
            ;;
        NOSUB_REPO)     DETECT_NOSUB_EXISTS="$content" ;;
        NAG_PATCHED)    DETECT_NAG_PATCHED="$content" ;;
        UPGRADABLE)     DETECT_UPGRADABLE_COUNT="${content//[^0-9]/}" ;;
        VMBR1)
            if [[ "$content" != "none" ]]; then
                DETECT_VMBR1_IP=$(echo "$content" | awk '{print $3}')
            fi
            ;;
        IP_FORWARD)     DETECT_IP_FORWARD="${content//[^01]/}" ;;
        NAT_ACTIVE)     DETECT_NAT_ACTIVE="$content" ;;
        WG_INSTALLED)   DETECT_WG_INSTALLED="$content" ;;
        WG_ACTIVE)
            if [[ "$content" != "none" ]]; then
                DETECT_WG_ACTIVE="yes"
            fi
            ;;
        WG_PEERS)       DETECT_WG_PEERS="${content//[^0-9]/}" ;;
        WG_HANDSHAKE)
            if [[ "$content" != "none" ]]; then
                DETECT_WG_HANDSHAKE="$content"
            fi
            ;;
        NFT_FILE)       DETECT_NFT_FILE="$content" ;;
        NFT_RULES)      DETECT_NFT_RULES="$content" ;;
        API_ROLE)
            if [[ "$content" == "yes" ]]; then DETECT_API_ROLE="yes"; fi
            ;;
        API_POOL)
            if [[ "$content" == "yes" ]]; then DETECT_API_POOL="yes"; fi
            ;;
        API_TOKEN)
            if [[ "$content" == "yes" ]]; then DETECT_API_TOKEN="yes"; fi
            ;;
        AVAILABLE_DISKS)
            DETECT_AVAILABLE_DISKS="$content"
            if [[ -n "$content" ]]; then
                DETECT_AVAILABLE_DISK_COUNT=$(echo "$content" | wc -l)
            else
                DETECT_AVAILABLE_DISK_COUNT="0"
            fi
            ;;
    esac
}

_compute_states() {
    # Repos
    if [[ -z "$DETECT_ENTERPRISE_ACTIVE" ]] && [[ "$DETECT_NOSUB_EXISTS" == "yes" ]] && [[ "$DETECT_NAG_PATCHED" == "yes" ]]; then
        STATE_REPOS="done"
    elif [[ "$DETECT_NOSUB_EXISTS" == "yes" ]] || [[ "$DETECT_NAG_PATCHED" == "yes" ]]; then
        STATE_REPOS="partial"
    else
        STATE_REPOS="needed"
    fi

    # Updates
    if [[ "${DETECT_UPGRADABLE_COUNT:-0}" -eq 0 ]]; then
        STATE_UPDATE="done"
    elif [[ "${DETECT_UPGRADABLE_COUNT:-0}" -gt 0 ]]; then
        STATE_UPDATE="needed"
    else
        STATE_UPDATE="unknown"
    fi

    # Bridge
    if [[ -n "$DETECT_VMBR1_IP" ]] && [[ "$DETECT_IP_FORWARD" == "1" ]] && [[ "$DETECT_NAT_ACTIVE" == "yes" ]]; then
        STATE_BRIDGE="done"
    elif [[ -n "$DETECT_VMBR1_IP" ]]; then
        STATE_BRIDGE="partial"
    else
        STATE_BRIDGE="needed"
    fi

    # Storage: check if any ZFS pool is registered as PVE storage
    local zfs_in_pve
    zfs_in_pve=$(echo "$DETECT_PVE_STORAGE" | grep -c "zfspool" || true)
    if [[ "$zfs_in_pve" -gt 0 ]]; then
        if [[ "$DETECT_AVAILABLE_DISK_COUNT" -gt 0 ]]; then
            STATE_STORAGE="available"  # has pool but also unused disks
        else
            STATE_STORAGE="done"
        fi
    elif [[ "$DETECT_AVAILABLE_DISK_COUNT" -gt 0 ]]; then
        STATE_STORAGE="needed"
    else
        STATE_STORAGE="done"  # no pool, but no disks available either
    fi

    # WireGuard
    if [[ "$DETECT_WG_ACTIVE" == "yes" ]]; then
        STATE_WIREGUARD="done"
    else
        STATE_WIREGUARD="needed"
    fi

    # Firewall
    if [[ "$DETECT_NFT_RULES" == "yes" ]]; then
        STATE_FIREWALL="done"
    else
        STATE_FIREWALL="needed"
    fi

    # API token
    if [[ "$DETECT_API_ROLE" == "yes" ]] && [[ "$DETECT_API_POOL" == "yes" ]] && [[ "$DETECT_API_TOKEN" == "yes" ]]; then
        STATE_API="done"
    elif [[ "$DETECT_API_ROLE" == "yes" ]] || [[ "$DETECT_API_POOL" == "yes" ]] || [[ "$DETECT_API_TOKEN" == "yes" ]]; then
        STATE_API="partial"
    else
        STATE_API="needed"
    fi
}

# ---------------------------------------------------------------------------
# Status dashboard
# ---------------------------------------------------------------------------

show_status() {
    echo ""
    echo -e "${BOLD}Host${RESET}          ${DETECT_HOSTNAME} (PVE ${DETECT_PVE_VERSION}, kernel ${DETECT_KERNEL})"
    echo -e "${BOLD}Public IP${RESET}     ${DETECT_PUBLIC_IP}"
    echo ""

    # Block devices
    echo -e "${BOLD}Block devices:${RESET}"
    echo "$DETECT_BLOCK_DEVICES" | while IFS= read -r line; do
        echo "  $line"
    done

    # ZFS pools
    if [[ -n "$DETECT_ZPOOLS" ]]; then
        echo ""
        echo -e "${BOLD}ZFS pools:${RESET}"
        echo "$DETECT_ZPOOLS" | while IFS= read -r line; do
            echo "  $line"
        done
    fi

    # Bridges
    echo ""
    echo -e "${BOLD}Network:${RESET}"
    if [[ -n "$DETECT_VMBR1_IP" ]]; then
        echo -e "  vmbr1       ${DETECT_VMBR1_IP}  (forwarding: ${DETECT_IP_FORWARD}, NAT: ${DETECT_NAT_ACTIVE})"
    else
        echo -e "  vmbr1       ${DIM}not configured${RESET}"
    fi
    if [[ "$DETECT_WG_ACTIVE" == "yes" ]]; then
        echo -e "  wg0         active  (${DETECT_WG_PEERS:-0} peer(s))"
    else
        echo -e "  wg0         ${DIM}not configured${RESET}"
    fi

    # Status dashboard
    echo ""
    echo -e "${BOLD}Configuration status:${RESET}"
    echo ""

    local effective_repos="$STATE_REPOS"
    local effective_update="$STATE_UPDATE"
    local effective_storage="$STATE_STORAGE"
    local effective_wg="$STATE_WIREGUARD"
    local effective_fw="$STATE_FIREWALL"

    if [[ "$SKIP_REPOS" == true ]]; then effective_repos="skipped"; effective_update="skipped"; fi
    if [[ "$SKIP_STORAGE" == true ]]; then effective_storage="skipped"; fi
    if [[ "$SKIP_WIREGUARD" == true ]]; then effective_wg="skipped"; fi
    if [[ "$SKIP_FIREWALL" == true ]]; then effective_fw="skipped"; fi

    printf "  %-20s %b" "Repositories" "$(status_icon "$effective_repos")"
    case "$effective_repos" in
        done)    echo -e "  no-subscription active, nag removed" ;;
        partial)
            local detail_parts=""
            [[ -z "$DETECT_ENTERPRISE_ACTIVE" ]] || detail_parts="enterprise still active"
            [[ "$DETECT_NOSUB_EXISTS" == "yes" ]] || detail_parts="${detail_parts:+$detail_parts, }no-sub repo missing"
            [[ "$DETECT_NAG_PATCHED" == "yes" ]] || detail_parts="${detail_parts:+$detail_parts, }nag popup present"
            echo "  $detail_parts"
            ;;
        needed)  echo "  enterprise repos active" ;;
        skipped) echo "" ;;
    esac

    printf "  %-20s %b" "System update" "$(status_icon "$effective_update")"
    case "$effective_update" in
        done)    echo "  all packages up to date" ;;
        needed)  echo "  ${DETECT_UPGRADABLE_COUNT} package(s) upgradable" ;;
        unknown) echo "  run apt update to check" ;;
        skipped) echo "" ;;
    esac

    printf "  %-20s %b" "Private bridge" "$(status_icon "$STATE_BRIDGE")"
    case "$STATE_BRIDGE" in
        done)    echo "  vmbr1 ${DETECT_VMBR1_IP}, NAT active" ;;
        partial) echo "  vmbr1 ${DETECT_VMBR1_IP} but forwarding/NAT incomplete" ;;
        needed)  echo "  vmbr1 not yet created" ;;
    esac

    printf "  %-20s %b" "Storage" "$(status_icon "$effective_storage")"
    case "$effective_storage" in
        done)      echo "  ZFS pool registered" ;;
        available) echo "  ZFS pool exists, ${DETECT_AVAILABLE_DISK_COUNT} unused disk(s) available" ;;
        needed)    echo "  ${DETECT_AVAILABLE_DISK_COUNT} unused disk(s), no pool created" ;;
        skipped)   echo "" ;;
    esac

    printf "  %-20s %b" "WireGuard" "$(status_icon "$effective_wg")"
    case "$effective_wg" in
        done)    echo "  wg0 active, ${DETECT_WG_PEERS:-0} peer(s)" ;;
        needed)
            if [[ "$DETECT_WG_INSTALLED" == "yes" ]]; then
                echo "  installed but not configured"
            else
                echo "  not installed"
            fi
            ;;
        skipped) echo "" ;;
    esac

    printf "  %-20s %b" "Firewall" "$(status_icon "$effective_fw")"
    case "$effective_fw" in
        done)    echo "  nftables rules active" ;;
        needed)
            if [[ "$DETECT_NFT_FILE" == "yes" ]]; then
                echo "  config file exists but rules not loaded"
            else
                echo "  no rules configured"
            fi
            ;;
        skipped) echo "" ;;
    esac

    printf "  %-20s %b" "API token" "$(status_icon "$STATE_API")"
    case "$STATE_API" in
        done)    echo "  role + pool + token configured" ;;
        partial)
            local missing=""
            [[ "$DETECT_API_ROLE" == "yes" ]]  || missing="role"
            [[ "$DETECT_API_POOL" == "yes" ]]  || missing="${missing:+$missing, }pool"
            [[ "$DETECT_API_TOKEN" == "yes" ]] || missing="${missing:+$missing, }token"
            echo "  missing: ${missing}"
            ;;
        needed)  echo "  not configured" ;;
    esac

    echo ""

    # Count pending items
    local pending=0
    if [[ "$effective_repos" == "needed" || "$effective_repos" == "partial" ]]; then pending=$((pending + 1)); fi
    if [[ "$effective_update" == "needed" ]]; then pending=$((pending + 1)); fi
    if [[ "$STATE_BRIDGE" == "needed" || "$STATE_BRIDGE" == "partial" ]]; then pending=$((pending + 1)); fi
    if [[ "$effective_storage" == "needed" ]]; then pending=$((pending + 1)); fi
    if [[ "$effective_wg" == "needed" ]]; then pending=$((pending + 1)); fi
    if [[ "$effective_fw" == "needed" ]]; then pending=$((pending + 1)); fi
    if [[ "$STATE_API" == "needed" || "$STATE_API" == "partial" ]]; then pending=$((pending + 1)); fi

    if [[ "$pending" -eq 0 ]]; then
        success "All configured -- nothing to do"
        return 1  # signal: no work needed
    fi

    info "${pending} item(s) need attention"
    echo ""
    return 0
}

# ---------------------------------------------------------------------------
# Apply: repos
# ---------------------------------------------------------------------------

apply_repos() {
    if [[ "$SKIP_REPOS" == true ]] || [[ "$STATE_REPOS" == "done" ]]; then
        return 0
    fi

    step "Repository configuration"

    # Show what needs fixing
    local actions=""
    if [[ -n "$DETECT_ENTERPRISE_ACTIVE" ]]; then
        actions="disable enterprise repos"
    fi
    if [[ "$DETECT_NOSUB_EXISTS" != "yes" ]]; then
        actions="${actions:+$actions, }add no-subscription repo"
    fi
    if [[ "$DETECT_NAG_PATCHED" != "yes" ]]; then
        actions="${actions:+$actions, }remove subscription nag popup"
    fi

    info "Will: ${actions}"

    if ! confirm "Apply repository changes?"; then
        info "Skipped"
        return 0
    fi

    if dry_run_guard "${actions}"; then
        return 0
    fi

    if [[ -n "$DETECT_ENTERPRISE_ACTIVE" ]]; then
        remote "sed -i 's/^deb/# deb/' /etc/apt/sources.list.d/pve-enterprise.list 2>/dev/null || true"
        remote "test -f /etc/apt/sources.list.d/ceph.list && sed -i 's/^deb/# deb/' /etc/apt/sources.list.d/ceph.list || true"
        success "Enterprise repos disabled"
    fi

    if [[ "$DETECT_NOSUB_EXISTS" != "yes" ]]; then
        remote "echo 'deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription' > /etc/apt/sources.list.d/pve-no-subscription.list"
        success "No-subscription repo added"
    fi

    if [[ "$DETECT_NAG_PATCHED" != "yes" ]]; then
        remote "sed -Ezi.bak \"s/(Ext\\.Msg\\.show\\(\\{[^}]+title: gettext\\('No valid sub)/void\\(\\{ \\/\\/\\1/\" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2>/dev/null || true"
        remote "systemctl restart pveproxy 2>/dev/null || true"
        success "Subscription nag removed"
    fi
}

# ---------------------------------------------------------------------------
# Apply: system update
# ---------------------------------------------------------------------------

apply_update() {
    if [[ "$SKIP_REPOS" == true ]] || [[ "$STATE_UPDATE" == "done" ]]; then
        return 0
    fi

    step "System update"

    if [[ "$STATE_UPDATE" == "unknown" ]]; then
        info "Package status unknown (apt update not yet run)"
    else
        info "${DETECT_UPGRADABLE_COUNT} package(s) can be upgraded"
    fi

    if ! confirm "Run apt update && apt dist-upgrade?"; then
        info "Skipped"
        return 0
    fi

    if dry_run_guard "apt update && apt dist-upgrade -y"; then
        return 0
    fi

    info "Updating package lists..."
    remote "apt update -qq"
    info "Upgrading packages (this may take a few minutes)..."
    remote "DEBIAN_FRONTEND=noninteractive apt dist-upgrade -y -qq"
    success "System updated"
}

# ---------------------------------------------------------------------------
# Apply: private bridge
# ---------------------------------------------------------------------------

apply_bridge() {
    if [[ "$STATE_BRIDGE" == "done" ]]; then
        return 0
    fi

    step "Private bridge (vmbr1)"

    if [[ "$STATE_BRIDGE" == "partial" ]]; then
        info "vmbr1 exists (${DETECT_VMBR1_IP}) but IP forwarding or NAT needs fixing"

        if [[ "$DETECT_IP_FORWARD" != "1" ]]; then
            if confirm "Enable IP forwarding?"; then
                if ! dry_run_guard "echo 1 > /proc/sys/net/ipv4/ip_forward"; then
                    remote "echo 1 > /proc/sys/net/ipv4/ip_forward"
                    remote_write "/etc/sysctl.d/99-forward.conf" "net.ipv4.ip_forward = 1"
                    success "IP forwarding enabled"
                fi
            fi
        fi

        if [[ "$DETECT_NAT_ACTIVE" != "yes" ]]; then
            if confirm "Add NAT masquerade rule for ${SUBNET}?"; then
                if ! dry_run_guard "iptables -t nat -A POSTROUTING -s ${SUBNET} -o vmbr0 -j MASQUERADE"; then
                    remote "iptables -t nat -A POSTROUTING -s ${SUBNET} -o vmbr0 -j MASQUERADE"
                    success "NAT masquerade active"
                    warn "Add post-up/post-down rules to /etc/network/interfaces to persist across reboots"
                fi
            fi
        fi
        return 0
    fi

    # STATE_BRIDGE == "needed"
    info "Will create vmbr1 with:"
    info "  Subnet:    ${SUBNET}"
    info "  Gateway:   ${SUBNET_GW}/${SUBNET_MASK}"
    info "  NAT:       MASQUERADE to vmbr0"

    local bridge_config
    bridge_config="
auto vmbr1
iface vmbr1 inet static
    address ${SUBNET_GW}/${SUBNET_MASK}
    bridge-ports none
    bridge-stp off
    bridge-fd 0
    post-up   echo 1 > /proc/sys/net/ipv4/ip_forward
    post-up   iptables -t nat -A POSTROUTING -s ${SUBNET} -o vmbr0 -j MASQUERADE
    post-down iptables -t nat -D POSTROUTING -s ${SUBNET} -o vmbr0 -j MASQUERADE"

    if [[ "$VERBOSE" == true ]]; then
        echo ""
        echo "$bridge_config"
        echo ""
    fi

    if ! confirm "Add vmbr1 to /etc/network/interfaces?"; then
        info "Skipped"
        return 0
    fi

    if dry_run_guard "Create vmbr1 (${SUBNET_GW}/${SUBNET_MASK})"; then
        return 0
    fi

    remote "printf '%s\n' '' >> /etc/network/interfaces"
    remote_write "/tmp/vmbr1-config.tmp" "$bridge_config"
    remote "cat /tmp/vmbr1-config.tmp >> /etc/network/interfaces && rm -f /tmp/vmbr1-config.tmp"

    remote_write "/etc/sysctl.d/99-forward.conf" "net.ipv4.ip_forward = 1"
    remote "sysctl -p /etc/sysctl.d/99-forward.conf >/dev/null 2>&1"

    remote "ifreload -a 2>/dev/null || ifup vmbr1"

    success "vmbr1 created (${SUBNET_GW}/${SUBNET_MASK})"
}

# ---------------------------------------------------------------------------
# Apply: storage
# ---------------------------------------------------------------------------

apply_storage() {
    if [[ "$SKIP_STORAGE" == true ]] || [[ "$STATE_STORAGE" == "done" ]]; then
        return 0
    fi

    step "ZFS storage pool"

    if [[ "$STATE_STORAGE" == "available" ]]; then
        info "ZFS pool already registered with Proxmox"
        info "${DETECT_AVAILABLE_DISK_COUNT} additional unused disk(s) available:"
        echo "$DETECT_AVAILABLE_DISKS" | while IFS= read -r line; do
            if [[ -n "$line" ]]; then echo "          /dev/$line"; fi
        done
        if ! confirm "Create an additional storage pool?"; then
            return 0
        fi
    fi

    if [[ "$STATE_STORAGE" == "needed" ]]; then
        # Check if a zpool named local-zfs already exists but isn't in PVE
        local existing_pool
        existing_pool=$(remote_output "zpool list -H local-zfs 2>/dev/null" || true)
        if [[ -n "$existing_pool" ]]; then
            info "ZFS pool 'local-zfs' exists but not registered with Proxmox"
            if confirm "Register local-zfs with Proxmox?"; then
                if ! dry_run_guard "pvesm add zfspool local-zfs"; then
                    remote "pvesm add zfspool local-zfs -pool local-zfs"
                    remote "pvesm set local-zfs -content images,rootdir"
                    success "local-zfs registered"
                fi
            fi
            return 0
        fi
    fi

    if [[ "$DETECT_AVAILABLE_DISK_COUNT" -eq 0 ]]; then
        info "No unused disks found"
        return 0
    fi

    info "Available disks:"
    echo "$DETECT_AVAILABLE_DISKS" | while IFS= read -r line; do
        if [[ -n "$line" ]]; then echo "          /dev/$line"; fi
    done

    local disk_count="$DETECT_AVAILABLE_DISK_COUNT"
    local disk_names
    disk_names=$(echo "$DETECT_AVAILABLE_DISKS" | awk 'NF{print "/dev/"$1}' | tr '\n' ' ')

    local pool_type
    if [[ "$disk_count" -eq 1 ]]; then
        pool_type="single"
        info "Recommendation: single disk (no redundancy)"
    elif [[ "$disk_count" -eq 2 ]]; then
        pool_type="mirror"
        info "Recommendation: mirror (2 disks, full redundancy)"
    else
        pool_type="raidz1"
        info "Recommendation: raidz1 (${disk_count} disks, 1-disk redundancy)"
    fi

    local pool_name
    pool_name=$(prompt_value "Pool name" "local-zfs")

    if ! confirm "Create ZFS pool '${pool_name}' (${pool_type}) with: ${disk_names}?"; then
        info "Skipped"
        return 0
    fi

    if dry_run_guard "zpool create ${pool_name} ${pool_type} ${disk_names}"; then
        return 0
    fi

    local zpool_cmd="zpool create ${pool_name}"
    case "$pool_type" in
        mirror) zpool_cmd="${zpool_cmd} mirror ${disk_names}" ;;
        raidz1) zpool_cmd="${zpool_cmd} raidz1 ${disk_names}" ;;
        single) zpool_cmd="${zpool_cmd} ${disk_names}" ;;
    esac

    remote "$zpool_cmd"
    remote "pvesm add zfspool ${pool_name} -pool ${pool_name}"
    remote "pvesm set ${pool_name} -content images,rootdir"

    success "ZFS pool '${pool_name}' created and registered"
}

# ---------------------------------------------------------------------------
# Apply: WireGuard
# ---------------------------------------------------------------------------

apply_wireguard() {
    if [[ "$SKIP_WIREGUARD" == true ]] || [[ "$STATE_WIREGUARD" == "done" ]]; then
        return 0
    fi

    step "WireGuard tunnel"

    info "Will configure WireGuard:"
    info "  Server:    ${WG_SERVER_IP}/${WG_SUBNET_MASK} on port ${WG_PORT}"
    info "  Client:    ${WG_CLIENT_IP}/${WG_SUBNET_MASK}"
    info "  Routing:   wg0 <-> vmbr1 forwarding"

    if ! confirm "Set up WireGuard?"; then
        info "Skipped"
        return 0
    fi

    if dry_run_guard "Install and configure WireGuard (${WG_SERVER_IP}:${WG_PORT})"; then
        return 0
    fi

    # Install if needed
    if [[ "$DETECT_WG_INSTALLED" != "yes" ]]; then
        info "Installing wireguard..."
        remote "DEBIAN_FRONTEND=noninteractive apt install -y -qq wireguard >/dev/null 2>&1"
    fi

    # Generate server keys
    info "Generating server keypair..."
    remote "wg genkey | tee /etc/wireguard/server-private.key | wg pubkey > /etc/wireguard/server-public.key"
    remote "chmod 600 /etc/wireguard/server-private.key"

    local server_privkey
    server_privkey=$(remote_output "cat /etc/wireguard/server-private.key")
    local server_pubkey
    server_pubkey=$(remote_output "cat /etc/wireguard/server-public.key")

    # Generate client keys locally
    info "Generating client keypair..."
    local client_privkey client_pubkey
    client_privkey=$(wg genkey)
    client_pubkey=$(echo "$client_privkey" | wg pubkey)

    # Write server config
    local server_conf="[Interface]
PrivateKey = ${server_privkey}
Address = ${WG_SERVER_IP}/${WG_SUBNET_MASK}
ListenPort = ${WG_PORT}

# Allow forwarding between WireGuard clients and the private VM bridge
PostUp = iptables -A FORWARD -i wg0 -o vmbr1 -j ACCEPT; iptables -A FORWARD -i vmbr1 -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -o vmbr1 -j ACCEPT; iptables -D FORWARD -i vmbr1 -o wg0 -j ACCEPT

[Peer]
# Workstation
PublicKey = ${client_pubkey}
AllowedIPs = ${WG_CLIENT_IP}/32"

    remote_write "/etc/wireguard/wg0.conf" "$server_conf"
    remote "chmod 600 /etc/wireguard/wg0.conf"

    remote "systemctl enable --now wg-quick@wg0"

    success "WireGuard server configured"

    echo ""
    info "Client config -- save to /etc/wireguard/hetzner-lab.conf or import into WireGuard app:"
    echo ""
    echo "  [Interface]"
    echo "  PrivateKey = ${client_privkey}"
    echo "  Address = ${WG_CLIENT_IP}/${WG_SUBNET_MASK}"
    echo ""
    echo "  [Peer]"
    echo "  PublicKey = ${server_pubkey}"
    echo "  Endpoint = ${DETECT_PUBLIC_IP}:${WG_PORT}"
    echo "  AllowedIPs = 10.10.0.0/16"
    echo "  PersistentKeepalive = 25"
    echo ""
    warn "Save the client config now -- private key is not stored anywhere else"
}

# ---------------------------------------------------------------------------
# Apply: firewall
# ---------------------------------------------------------------------------

apply_firewall() {
    if [[ "$SKIP_FIREWALL" == true ]] || [[ "$STATE_FIREWALL" == "done" ]]; then
        return 0
    fi

    step "Firewall lockdown (nftables)"

    if [[ "$DETECT_NFT_FILE" == "yes" ]]; then
        info "Config file exists but rules not loaded"
        if confirm "Load existing firewall rules from /etc/nftables-hetzner.conf?"; then
            if ! dry_run_guard "nft -f /etc/nftables-hetzner.conf"; then
                remote "nft -f /etc/nftables-hetzner.conf"
                remote "systemctl enable nftables"
                success "Firewall rules loaded"
            fi
        fi
        return 0
    fi

    info "Will restrict public interface to SSH (22) and WireGuard (${WG_PORT}) only"
    info "Web UI (8006) and VM traffic only via WireGuard"
    warn "Make sure WireGuard is working before applying firewall rules!"

    if ! confirm "Apply firewall rules?"; then
        info "Skipped"
        return 0
    fi

    if dry_run_guard "Apply nftables rules (SSH + WG only on public interface)"; then
        return 0
    fi

    local nft_rules="#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # Loopback
        iif lo accept

        # Established/related connections
        ct state established,related accept

        # ICMP (ping)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # SSH on public interface
        iifname \"vmbr0\" tcp dport 22 accept

        # WireGuard on public interface
        iifname \"vmbr0\" udp dport ${WG_PORT} accept

        # Allow everything on private bridge and WireGuard
        iifname \"vmbr1\" accept
        iifname \"wg0\" accept

        # Log and drop everything else
        log prefix \"nft-drop: \" limit rate 5/minute counter drop
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        # Established/related
        ct state established,related accept

        # WireGuard <-> private bridge
        iifname \"wg0\" oifname \"vmbr1\" accept
        iifname \"vmbr1\" oifname \"wg0\" accept

        # Private bridge -> internet (NAT)
        iifname \"vmbr1\" oifname \"vmbr0\" accept
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}"

    remote_write "/etc/nftables-hetzner.conf" "$nft_rules"
    remote "nft -f /etc/nftables-hetzner.conf"
    remote "cp /etc/nftables-hetzner.conf /etc/nftables.conf"
    remote "systemctl enable nftables"

    success "Firewall applied (SSH + WireGuard only on public interface)"
}

# ---------------------------------------------------------------------------
# Apply: API token
# ---------------------------------------------------------------------------

apply_api_token() {
    if [[ "$STATE_API" == "done" ]]; then
        return 0
    fi

    step "API token for incusos-proxmox"

    # Show what's missing
    local actions=""
    [[ "$DETECT_API_ROLE" == "yes" ]]  || actions="create IncusOSDeployer role"
    [[ "$DETECT_API_POOL" == "yes" ]]  || actions="${actions:+$actions, }create IncusLab pool"
    [[ "$DETECT_API_TOKEN" == "yes" ]] || actions="${actions:+$actions, }create automation@pve!deploy token"

    info "Will: ${actions}"

    if ! confirm "Create missing API resources?"; then
        info "Skipped"
        return 0
    fi

    if dry_run_guard "${actions}"; then
        return 0
    fi

    # Create role if needed
    if [[ "$DETECT_API_ROLE" != "yes" ]]; then
        remote "pveum role add IncusOSDeployer -privs \
            'VM.Allocate VM.Config.CDROM VM.Config.CPU VM.Config.Disk VM.Config.HWType \
             VM.Config.Memory VM.Config.Network VM.Config.Options VM.PowerMgmt \
             VM.Monitor VM.Audit VM.Console \
             Datastore.AllocateSpace Datastore.Audit \
             Pool.Allocate Pool.Audit \
             SDN.Use Sys.Modify'"
        success "Created IncusOSDeployer role"
    fi

    # Create pool if needed
    if [[ "$DETECT_API_POOL" != "yes" ]]; then
        remote "pveum pool add IncusLab -comment 'IncusOS lab VMs'"
        success "Created IncusLab pool"
    fi

    # Create token if needed
    if [[ "$DETECT_API_TOKEN" != "yes" ]]; then
        # Ensure user exists
        remote "pveum user add automation@pve --comment 'IncusOS automation' 2>/dev/null || true"

        local token_output
        token_output=$(remote "pveum user token add automation@pve deploy --privsep 0 --output-format json")

        local token_secret
        token_secret=$(echo "$token_output" | python3 -c "import sys,json; print(json.load(sys.stdin)['value'])" 2>/dev/null || echo "$token_output")

        # Assign ACLs
        remote "pveum acl modify /pool/IncusLab -user automation@pve -role IncusOSDeployer"

        local storage_name
        storage_name=$(remote_output "pvesm status 2>/dev/null | awk '/zfspool/{print \$1}' | head -1" || true)
        if [[ -n "$storage_name" ]]; then
            remote "pveum acl modify /storage/${storage_name} -user automation@pve -role IncusOSDeployer"
        fi
        remote "pveum acl modify /storage/local -user automation@pve -role IncusOSDeployer"

        success "API token created"
        echo ""
        warn "Token secret (save now -- shown only once):"
        echo ""
        echo "  ${token_secret}"
        echo ""
    fi
}

# ---------------------------------------------------------------------------
# Summary output
# ---------------------------------------------------------------------------

show_summary() {
    step "Summary"

    echo ""
    echo -e "${BOLD}proxmox.yaml for incusos/targets/hetzner/:${RESET}"
    echo ""
    echo "  host: ${SUBNET_GW}"
    echo "  method: api"
    echo "  api_token_id: automation@pve!deploy"
    echo "  node: ${DETECT_HOSTNAME:-pve}"
    echo "  storage: local-zfs"
    echo "  iso_storage: local"
    echo "  bridge: vmbr1"
    echo "  gateway: ${SUBNET_GW}"
    echo "  dns: ${SUBNET_GW}"
    echo "  pool: IncusLab"
    echo "  ssh_user: root"

    echo ""
    echo -e "${BOLD}env file additions:${RESET}"
    echo ""
    echo "  # Hetzner"
    echo "  HETZNER_PROXMOX_TOKEN_SECRET=<token from step above>"
    echo "  HETZNER_PROXMOX_ROOT_PASSWORD=<your root password>"

    echo ""
    echo -e "${BOLD}Next steps:${RESET}"
    echo ""
    echo "  1. Save WireGuard client config and connect"
    echo "  2. Copy proxmox.yaml.example and fill in credentials:"
    echo "     cp incusos/targets/hetzner/proxmox.yaml.example incusos/targets/hetzner/proxmox.yaml"
    echo "  3. Test with dry run:"
    echo "     export PROXMOX_TOKEN_SECRET=\"\$HETZNER_PROXMOX_TOKEN_SECRET\""
    echo "     incusos-proxmox --dry-run --proxmox incusos/targets/hetzner/proxmox.yaml \\"
    echo "                     incusos/targets/hetzner/lab-cluster.yaml"
    echo ""
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

main() {
    setup_colors
    parse_args "$@"
    parse_subnets

    echo ""
    echo -e "${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} -- Proxmox post-install configuration"
    if [[ "$DRY_RUN" == true ]]; then
        echo -e "${YELLOW}(dry-run mode -- no changes will be made)${RESET}"
    fi
    echo ""

    # Phase 1: detect everything
    detect_all

    # Phase 2: show status dashboard
    if ! show_status; then
        # All done, nothing to do
        show_summary
        exit 0
    fi

    # Phase 3: apply only what's needed
    if ! confirm "Proceed with configuration?"; then
        info "Exiting without changes"
        exit 0
    fi

    echo ""
    apply_repos
    apply_update
    apply_bridge
    apply_storage
    apply_wireguard
    apply_firewall
    apply_api_token
    show_summary

    success "Setup complete"
}

main "$@"
