#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: tteck (tteckster) | MickLesk | michelroegl-brunner
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/branch/main/LICENSE

# ==============================================================================
# BUILD.FUNC - LXC CONTAINER BUILD & CONFIGURATION
# ==============================================================================
#
# This file provides the main build functions for creating and configuring
# LXC containers in Proxmox VE. It handles:
#
#   - Variable initialization and defaults
#   - Container creation and resource allocation
#   - Storage selection and management
#   - Advanced configuration and customization
#   - User interaction menus and prompts
#
# Usage:
#   - Sourced automatically by CT creation scripts
#   - Requires core.func and error_handler.func to be loaded first
#
# ==============================================================================

# ==============================================================================
# SECTION 1: INITIALIZATION & CORE VARIABLES
# ==============================================================================

# ------------------------------------------------------------------------------
# variables()
#
# - Initializes core variables for container creation
# - Normalizes application name (NSAPP = lowercase, no spaces)
# - Builds installer filename (var_install)
# - Defines regex patterns for validation
# - Fetches Proxmox hostname and version
# - Generates unique session ID for tracking and logging
# - Captures app-declared resource defaults (CPU, RAM, Disk)
# ------------------------------------------------------------------------------
variables() {
  NSAPP=$(echo "${APP,,}" | tr -d ' ')              # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
  var_install="${NSAPP}-install"                    # sets the var_install variable by appending "-install" to the value of NSAPP.
  INTEGER='^[0-9]+([.][0-9]+)?$'                    # it defines the INTEGER regular expression pattern.
  PVEHOST_NAME=$(hostname)                          # gets the Proxmox Hostname and sets it to Uppercase
  DIAGNOSTICS="no"                                  # Safe default: no telemetry until user consents via diagnostics_check()
  METHOD="default"                                  # sets the METHOD variable to "default", used for the API call.
  RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
  EXECUTION_ID="${RANDOM_UUID}"                     # Unique execution ID for telemetry record identification (unique-indexed in PocketBase)
  SESSION_ID="${RANDOM_UUID:0:8}"                   # Short session ID (first 8 chars of UUID) for log files
  BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log"     # Host-side container creation log
  # NOTE: combined_log is constructed locally in build_container() and ensure_log_on_host()
  # as "/tmp/${NSAPP}-${CTID}-${SESSION_ID}.log" (requires CTID, not available here)
  CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"

  # Parse dev_mode early
  parse_dev_mode

  # Setup persistent log directory if logs mode active
  if [[ "${DEV_MODE_LOGS:-false}" == "true" ]]; then
    mkdir -p /var/log/community-scripts
    BUILD_LOG="/var/log/community-scripts/create-lxc-${SESSION_ID}-$(date +%Y%m%d_%H%M%S).log"
  fi

  # Get Proxmox VE version and kernel version
  if command -v pveversion >/dev/null 2>&1; then
    PVEVERSION="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
  else
    PVEVERSION="N/A"
  fi
  KERNEL_VERSION=$(uname -r)

  # Capture app-declared defaults (for precedence logic)
  # These values are set by the app script BEFORE default.vars is loaded
  # If app declares higher values than default.vars, app values take precedence
  if [[ -n "${var_cpu:-}" && "${var_cpu}" =~ ^[0-9]+$ ]]; then
    export APP_DEFAULT_CPU="${var_cpu}"
  fi
  if [[ -n "${var_ram:-}" && "${var_ram}" =~ ^[0-9]+$ ]]; then
    export APP_DEFAULT_RAM="${var_ram}"
  fi
  if [[ -n "${var_disk:-}" && "${var_disk}" =~ ^[0-9]+$ ]]; then
    export APP_DEFAULT_DISK="${var_disk}"
  fi
}

source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)

if command -v curl >/dev/null 2>&1; then
  source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
  source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
  load_functions
  catch_errors
elif command -v wget >/dev/null 2>&1; then
  source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
  source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
  load_functions
  catch_errors
fi

# ==============================================================================
# SECTION 2: PRE-FLIGHT CHECKS & SYSTEM VALIDATION
# ==============================================================================

# ------------------------------------------------------------------------------
# maxkeys_check()
#
# - Reads kernel keyring limits (maxkeys, maxbytes)
# - Checks current usage for LXC user (UID 100000)
# - Warns if usage is close to limits and suggests sysctl tuning
# - Exits if thresholds are exceeded
# - https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
# ------------------------------------------------------------------------------

maxkeys_check() {
  # Read kernel parameters
  per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
  per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)

  # Exit if kernel parameters are unavailable
  if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
    msg_error "Unable to read kernel key parameters. Ensure proper permissions."
    exit 107
  fi

  # Fetch key usage for user ID 100000 (typical for containers)
  used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
  used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)

  # Calculate thresholds and suggested new limits
  threshold_keys=$((per_user_maxkeys - 100))
  threshold_bytes=$((per_user_maxbytes - 1000))
  new_limit_keys=$((per_user_maxkeys * 2))
  new_limit_bytes=$((per_user_maxbytes * 2))

  # Check if key or byte usage is near limits
  failure=0
  if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
    msg_warn "Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys})"
    echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
    failure=1
  fi
  if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
    msg_warn "Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes})"
    echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
    failure=1
  fi

  # Provide next steps if issues are detected
  if [[ "$failure" -eq 1 ]]; then
    msg_error "Kernel key limits exceeded - see suggestions above"
    exit 108
  fi

  # Silent success - only show errors if they exist
}

# ==============================================================================
# SECTION 3: CONTAINER SETUP UTILITIES
# ==============================================================================

# ------------------------------------------------------------------------------
# get_current_ip()
#
# - Returns current container IP depending on OS type
# - Debian/Ubuntu: uses `hostname -I`
# - Alpine: parses eth0 via `ip -4 addr` or `ip -6 addr`
# - Supports IPv6-only environments as fallback
# - Returns "Unknown" if OS type cannot be determined
# ------------------------------------------------------------------------------
get_current_ip() {
  CURRENT_IP=""
  if [ -f /etc/os-release ]; then
    # Check for Debian/Ubuntu (uses hostname -I)
    if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
      # Try IPv4 first
      CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
      # Fallback to IPv6 if no IPv4
      if [[ -z "$CURRENT_IP" ]]; then
        CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1 || true)
      fi
    # Check for Alpine (uses ip command)
    elif grep -q 'ID=alpine' /etc/os-release; then
      # Try IPv4 first
      CURRENT_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
      # Fallback to IPv6 if no IPv4
      if [[ -z "$CURRENT_IP" ]]; then
        CURRENT_IP=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n 1)
      fi
    else
      CURRENT_IP="Unknown"
    fi
  fi
  echo "$CURRENT_IP"
}

# ------------------------------------------------------------------------------
# update_motd_ip()
#
# - Updates /etc/motd with current container IP
# - Removes old IP entries to avoid duplicates
# - Regenerates /etc/profile.d/00_lxc-details.sh with dynamic OS/IP info
# ------------------------------------------------------------------------------
update_motd_ip() {
  MOTD_FILE="/etc/motd"
  PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"

  if [ -f "$MOTD_FILE" ]; then
    # Remove existing IP Address lines to prevent duplication
    sed -i '/IP Address:/d' "$MOTD_FILE"

    IP=$(get_current_ip)
    # Add the new IP address
    echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
  fi

  # Update dynamic LXC details profile if values changed (e.g., after OS upgrade)
  # Only update if file exists and is from community-scripts
  if [ -f "$PROFILE_FILE" ] && grep -q "community-scripts" "$PROFILE_FILE" 2>/dev/null; then
    # Get current values
    local current_os="$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')"
    local current_hostname="$(hostname)"
    local current_ip="$(hostname -I | awk '{print $1}')"

    # Escape sed special chars in replacement strings (& \ |)
    current_os="${current_os//\\/\\\\}"
    current_os="${current_os//&/\\&}"
    current_hostname="${current_hostname//\\/\\\\}"
    current_hostname="${current_hostname//&/\\&}"
    current_ip="${current_ip//\\/\\\\}"
    current_ip="${current_ip//&/\\&}"

    # Update only if values actually changed
    if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then
      sed -i "s|OS:.*|OS: \${GN}$current_os\${CL}\\\"|" "$PROFILE_FILE"
    fi
    if ! grep -q "Hostname:.*$current_hostname" "$PROFILE_FILE" 2>/dev/null; then
      sed -i "s|Hostname:.*|Hostname: \${GN}$current_hostname\${CL}\\\"|" "$PROFILE_FILE"
    fi
    if ! grep -q "IP Address:.*$current_ip" "$PROFILE_FILE" 2>/dev/null; then
      sed -i "s|IP Address:.*|IP Address: \${GN}$current_ip\${CL}\\\"|" "$PROFILE_FILE"
    fi
  fi
}

# ------------------------------------------------------------------------------
# install_ssh_keys_into_ct()
#
# - Installs SSH keys into container root account if SSH is enabled
# - Uses pct push or direct input to authorized_keys
# - Supports both SSH_KEYS_FILE (from advanced settings) and SSH_AUTHORIZED_KEY (from user defaults)
# - Falls back to warning if no keys provided
# ------------------------------------------------------------------------------
install_ssh_keys_into_ct() {
  [[ "${SSH:-no}" != "yes" ]] && return 0

  # Ensure SSH_KEYS_FILE is defined (may not be set if advanced_settings was skipped)
  : "${SSH_KEYS_FILE:=}"

  # If SSH_KEYS_FILE doesn't exist but SSH_AUTHORIZED_KEY is set (from user defaults),
  # create a temporary SSH_KEYS_FILE with the key
  if [[ -z "$SSH_KEYS_FILE" || ! -s "$SSH_KEYS_FILE" ]] && [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then
    SSH_KEYS_FILE="$(mktemp)"
    printf '%s\n' "$SSH_AUTHORIZED_KEY" >"$SSH_KEYS_FILE"
  fi

  if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then
    msg_info "Installing selected SSH keys into CT ${CTID}"
    pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || {
      msg_error "prepare /root/.ssh failed"
      return 252
    }
    pct push "$CTID" "$SSH_KEYS_FILE" /root/.ssh/authorized_keys >/dev/null 2>&1 ||
      pct exec "$CTID" -- sh -c "cat > /root/.ssh/authorized_keys" <"$SSH_KEYS_FILE" || {
      msg_error "write authorized_keys failed"
      return 252
    }
    pct exec "$CTID" -- sh -c 'chmod 600 /root/.ssh/authorized_keys' || true
    msg_ok "Installed SSH keys into CT ${CTID}"
    return 0
  fi

  # Fallback
  msg_warn "No SSH keys to install (skipping)."
  return 0
}

# ------------------------------------------------------------------------------
# validate_container_id()
#
# - Validates if a container ID is available for use (CLUSTER-WIDE)
# - Checks cluster resources via pvesh for VMs/CTs on ALL nodes
# - Falls back to local config file check if pvesh unavailable
# - Checks if ID is used in LVM logical volumes
# - Returns 0 if ID is available, 1 if already in use
# ------------------------------------------------------------------------------
validate_container_id() {
  local ctid="$1"

  # Check if ID is numeric
  if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
    return 1
  fi

  # CLUSTER-WIDE CHECK: Query all VMs/CTs across all nodes
  # This catches IDs used on other nodes in the cluster
  # NOTE: Works on single-node too - Proxmox always has internal cluster structure
  # Falls back gracefully if pvesh unavailable or returns empty
  if command -v pvesh &>/dev/null; then
    local cluster_ids
    cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
      grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
    if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
      return 1
    fi
  fi

  # LOCAL FALLBACK: Check if config file exists for VM or LXC
  # This handles edge cases where pvesh might not return all info
  if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
    return 1
  fi

  # Check ALL nodes in cluster for config files (handles pmxcfs sync delays)
  # NOTE: On single-node, /etc/pve/nodes/ contains just the one node - still works
  if [[ -d "/etc/pve/nodes" ]]; then
    for node_dir in /etc/pve/nodes/*/; do
      if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
        return 1
      fi
    done
  fi

  # Check if ID is used in LVM logical volumes
  if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
    return 1
  fi

  return 0
}

# ------------------------------------------------------------------------------
# get_valid_container_id()
#
# - Returns a valid, unused container ID (CLUSTER-AWARE)
# - Uses pvesh /cluster/nextid as starting point (already cluster-aware)
# - If provided ID is valid, returns it
# - Otherwise increments until a free one is found across entire cluster
# - Calls validate_container_id() to check availability
# ------------------------------------------------------------------------------
get_valid_container_id() {
  local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}"

  # Ensure we have a valid starting ID
  if ! [[ "$suggested_id" =~ ^[0-9]+$ ]]; then
    suggested_id=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
  fi

  local max_attempts=1000
  local attempts=0

  while ! validate_container_id "$suggested_id"; do
    suggested_id=$((suggested_id + 1))
    attempts=$((attempts + 1))
    if [[ $attempts -ge $max_attempts ]]; then
      msg_error "Could not find available container ID after $max_attempts attempts"
      exit 109
    fi
  done

  echo "$suggested_id"
}

# ------------------------------------------------------------------------------
# validate_hostname()
#
# - Validates hostname/FQDN according to RFC 1123/952
# - Checks total length (max 253 characters for FQDN)
# - Validates each label (max 63 chars, alphanumeric + hyphens)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_hostname() {
  local hostname="$1"

  # Check total length (max 253 for FQDN)
  if [[ ${#hostname} -gt 253 ]] || [[ -z "$hostname" ]]; then
    return 1
  fi

  # Split by dots and validate each label
  local IFS='.'
  read -ra labels <<<"$hostname"
  for label in "${labels[@]}"; do
    # Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end)
    if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then
      return 1
    fi
    if [[ ! "$label" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] && [[ ! "$label" =~ ^[a-z0-9]$ ]]; then
      return 1
    fi
  done

  return 0
}

# ------------------------------------------------------------------------------
# validate_mac_address()
#
# - Validates MAC address format (XX:XX:XX:XX:XX:XX)
# - Empty value is allowed (auto-generated)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_mac_address() {
  local mac="$1"
  [[ -z "$mac" ]] && return 0
  if [[ ! "$mac" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then
    return 1
  fi
  return 0
}

# ------------------------------------------------------------------------------
# validate_vlan_tag()
#
# - Validates VLAN tag (1-4094)
# - Empty value is allowed (no VLAN)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_vlan_tag() {
  local vlan="$1"
  [[ -z "$vlan" ]] && return 0
  if ! [[ "$vlan" =~ ^[0-9]+$ ]] || ((vlan < 1 || vlan > 4094)); then
    return 1
  fi
  return 0
}

# ------------------------------------------------------------------------------
# validate_mtu()
#
# - Validates MTU size (576-65535, common values: 1500, 9000)
# - Empty value is allowed (default 1500)
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_mtu() {
  local mtu="$1"
  [[ -z "$mtu" ]] && return 0
  if ! [[ "$mtu" =~ ^[0-9]+$ ]] || ((mtu < 576 || mtu > 65535)); then
    return 1
  fi
  return 0
}

# ------------------------------------------------------------------------------
# validate_ipv6_address()
#
# - Validates IPv6 address with optional CIDR notation
# - Supports compressed (::) and full notation
# - Empty value is allowed
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_ipv6_address() {
  local ipv6="$1"
  [[ -z "$ipv6" ]] && return 0

  # Extract address and CIDR
  local addr="${ipv6%%/*}"
  local cidr="${ipv6##*/}"

  # Validate CIDR if present (1-128)
  if [[ "$ipv6" == */* ]]; then
    if ! [[ "$cidr" =~ ^[0-9]+$ ]] || ((cidr < 1 || cidr > 128)); then
      return 1
    fi
  fi

  # Basic IPv6 validation - check for valid characters and structure
  # Must contain only hex digits and colons
  if [[ ! "$addr" =~ ^[0-9a-fA-F:]+$ ]]; then
    return 1
  fi

  # Must contain at least one colon
  if [[ ! "$addr" == *:* ]]; then
    return 1
  fi

  # Check for valid double-colon usage (only one :: allowed)
  if [[ "$addr" == *::*::* ]]; then
    return 1
  fi

  # Check that no segment exceeds 4 hex chars
  local IFS=':'
  local -a segments
  read -ra segments <<<"$addr"
  for seg in "${segments[@]}"; do
    if [[ ${#seg} -gt 4 ]]; then
      return 1
    fi
  done

  return 0
}

# ------------------------------------------------------------------------------
# validate_bridge()
#
# - Validates that network bridge exists and is active
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_bridge() {
  local bridge="$1"
  [[ -z "$bridge" ]] && return 1

  # Check if bridge interface exists
  if ! ip link show "$bridge" &>/dev/null; then
    return 1
  fi

  return 0
}

# ------------------------------------------------------------------------------
# validate_gateway_in_subnet()
#
# - Validates that gateway IP is in the same subnet as static IP
# - Arguments: static_ip (with CIDR), gateway_ip
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_gateway_in_subnet() {
  local static_ip="$1"
  local gateway="$2"

  [[ -z "$static_ip" || -z "$gateway" ]] && return 0

  # Extract IP and CIDR
  local ip="${static_ip%%/*}"
  local cidr="${static_ip##*/}"

  # /31 and /32 are valid point-to-point / zero-trust DMZ configurations
  # where the gateway is technically outside the subnet — skip validation
  ((cidr >= 31)) && return 0

  # Convert CIDR to netmask bits
  local mask=$((0xFFFFFFFF << (32 - cidr) & 0xFFFFFFFF))

  # Convert IPs to integers
  local IFS='.'
  read -r i1 i2 i3 i4 <<<"$ip"
  read -r g1 g2 g3 g4 <<<"$gateway"

  local ip_int=$(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
  local gw_int=$(((g1 << 24) + (g2 << 16) + (g3 << 8) + g4))

  # Check if both are in same network
  if (((ip_int & mask) != (gw_int & mask))); then
    return 1
  fi

  return 0
}

# ------------------------------------------------------------------------------
# validate_ip_address()
#
# - Validates IPv4 address with CIDR notation
# - Checks each octet is 0-255
# - Checks CIDR is 1-32
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_ip_address() {
  local ip="$1"
  [[ -z "$ip" ]] && return 1

  # Check format with CIDR
  if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$ ]]; then
    return 1
  fi

  local o1="${BASH_REMATCH[1]}"
  local o2="${BASH_REMATCH[2]}"
  local o3="${BASH_REMATCH[3]}"
  local o4="${BASH_REMATCH[4]}"
  local cidr="${BASH_REMATCH[5]}"

  # Validate octets (0-255)
  for octet in "$o1" "$o2" "$o3" "$o4"; do
    if ((octet > 255)); then
      return 1
    fi
  done

  # Validate CIDR (1-32)
  if ((cidr < 1 || cidr > 32)); then
    return 1
  fi

  return 0
}

# ------------------------------------------------------------------------------
# validate_gateway_ip()
#
# - Validates gateway IPv4 address (without CIDR)
# - Checks each octet is 0-255
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_gateway_ip() {
  local ip="$1"
  [[ -z "$ip" ]] && return 0

  # Check format without CIDR
  if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
    return 1
  fi

  local o1="${BASH_REMATCH[1]}"
  local o2="${BASH_REMATCH[2]}"
  local o3="${BASH_REMATCH[3]}"
  local o4="${BASH_REMATCH[4]}"

  # Validate octets (0-255)
  for octet in "$o1" "$o2" "$o3" "$o4"; do
    if ((octet > 255)); then
      return 1
    fi
  done

  return 0
}

# ------------------------------------------------------------------------------
# validate_timezone()
#
# - Validates timezone string against system zoneinfo
# - Empty value or "host" is allowed
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_timezone() {
  local tz="$1"
  [[ -z "$tz" || "$tz" == "host" ]] && return 0

  # Check if timezone file exists
  if [[ ! -f "/usr/share/zoneinfo/$tz" ]]; then
    return 1
  fi

  return 0
}

# ------------------------------------------------------------------------------
# validate_tags()
#
# - Validates Proxmox tags format
# - Only alphanumeric, hyphens, underscores, and semicolons allowed
# - Empty value is allowed
# - Returns 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_tags() {
  local tags="$1"
  [[ -z "$tags" ]] && return 0

  # Tags can only contain alphanumeric, -, _, and ; (separator)
  if [[ ! "$tags" =~ ^[a-zA-Z0-9_\;-]+$ ]]; then
    return 1
  fi

  return 0
}

# ------------------------------------------------------------------------------
# find_host_ssh_keys()
#
# - Scans system for available SSH keys
# - Supports defaults (~/.ssh, /etc/ssh/authorized_keys)
# - Returns list of files containing valid SSH public keys
# - Sets FOUND_HOST_KEY_COUNT to number of keys found
# ------------------------------------------------------------------------------
find_host_ssh_keys() {
  local re='(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))'
  local -a files=() cand=()
  local g="${var_ssh_import_glob:-}"
  local total=0 f base c

  shopt -s nullglob
  if [[ -n "$g" ]]; then
    for pat in $g; do cand+=($pat); done
  else
    cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
    cand+=(/root/.ssh/*.pub)
    cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
  fi
  shopt -u nullglob

  for f in "${cand[@]}"; do
    [[ -f "$f" && -r "$f" ]] || continue
    base="$(basename -- "$f")"
    case "$base" in
    known_hosts | known_hosts.* | config) continue ;;
    id_*) [[ "$f" != *.pub ]] && continue ;;
    esac

    # CRLF safe check for host keys
    c=$(tr -d '\r' <"$f" | awk '
      /^[[:space:]]*#/ {next}
      /^[[:space:]]*$/ {next}
      {print}
    ' | grep -E -c "$re" || true)

    if ((c > 0)); then
      files+=("$f")
      total=$((total + c))
    fi
  done

  # Fallback to /root/.ssh/authorized_keys
  if ((${#files[@]} == 0)) && [[ -r /root/.ssh/authorized_keys ]]; then
    if grep -E -q "$re" /root/.ssh/authorized_keys; then
      files+=(/root/.ssh/authorized_keys)
      total=$((total + $(grep -E -c "$re" /root/.ssh/authorized_keys || echo 0)))
    fi
  fi

  FOUND_HOST_KEY_COUNT="$total"
  (
    IFS=:
    echo "${files[*]}"
  )
}

# ==============================================================================
# SECTION 3B: IP RANGE SCANNING
# ==============================================================================

# ------------------------------------------------------------------------------
# ip_to_int() / int_to_ip()
#
# - Converts IP address to integer and vice versa for range iteration
# ------------------------------------------------------------------------------
ip_to_int() {
  local IFS=.
  read -r i1 i2 i3 i4 <<<"$1"
  echo $(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
}

int_to_ip() {
  local ip=$1
  echo "$(((ip >> 24) & 0xFF)).$(((ip >> 16) & 0xFF)).$(((ip >> 8) & 0xFF)).$((ip & 0xFF))"
}

# ------------------------------------------------------------------------------
# resolve_ip_from_range()
#
# - Takes an IP range in format "10.0.0.1/24-10.0.0.10/24"
# - Pings each IP in the range to find the first available one
# - Returns the first free IP with CIDR notation
# - Sets NET_RESOLVED to the resolved IP or empty on failure
# ------------------------------------------------------------------------------
resolve_ip_from_range() {
  local range="$1"
  local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
  local ip_start ip_end

  # Parse range: "10.0.0.1/24-10.0.0.10/24"
  ip_start="${range%%-*}"
  ip_end="${range##*-}"

  if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then
    NET_RESOLVED=""
    return 1
  fi

  local ip1="${ip_start%%/*}"
  local ip2="${ip_end%%/*}"
  local cidr="${ip_start##*/}"

  local start_int=$(ip_to_int "$ip1")
  local end_int=$(ip_to_int "$ip2")

  for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do
    local ip=$(int_to_ip $ip_int)
    msg_info "Checking IP: $ip"
    if ! ping -c 1 -W 1 "$ip" >/dev/null 2>&1; then
      NET_RESOLVED="$ip/$cidr"
      msg_ok "Found free IP: ${BGN}$NET_RESOLVED${CL}"
      return 0
    fi
  done

  NET_RESOLVED=""
  msg_error "No free IP found in range $range"
  return 1
}

# ------------------------------------------------------------------------------
# is_ip_range()
#
# - Checks if a string is an IP range (contains - and looks like IP/CIDR)
# - Returns 0 if it's a range, 1 otherwise
# ------------------------------------------------------------------------------
is_ip_range() {
  local value="$1"
  local ip_start ip_end
  if [[ "$value" == *-* ]] && [[ "$value" != "dhcp" ]]; then
    local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
    ip_start="${value%%-*}"
    ip_end="${value##*-}"
    if [[ "$ip_start" =~ $ip_cidr_regex ]] && [[ "$ip_end" =~ $ip_cidr_regex ]]; then
      return 0
    fi
  fi
  return 1
}

# ==============================================================================
# SECTION 4: STORAGE & RESOURCE MANAGEMENT
# ==============================================================================

# ------------------------------------------------------------------------------
# _write_storage_to_vars()
#
# - Writes storage selection to vars file
# - Removes old entries (commented and uncommented) to avoid duplicates
# - Arguments: vars_file, key (var_container_storage/var_template_storage), value
# ------------------------------------------------------------------------------
_write_storage_to_vars() {
  # $1 = vars_file, $2 = key (var_container_storage / var_template_storage), $3 = value
  local vf="$1" key="$2" val="$3"
  # remove uncommented and commented versions to avoid duplicates
  sed -i "/^[#[:space:]]*${key}=/d" "$vf"
  echo "${key}=${val}" >>"$vf"
}

choose_and_set_storage_for_file() {
  # $1 = vars_file, $2 = class ('container'|'template')
  local vf="$1" class="$2" key="" current=""
  case "$class" in
  container) key="var_container_storage" ;;
  template) key="var_template_storage" ;;
  *)
    msg_error "Unknown storage class: $class"
    return 65
    ;;
  esac

  current=$(awk -F= -v k="^${key}=" '$0 ~ k {print $2; exit}' "$vf")

  # If only one storage exists for the content type, auto-pick. Else always ask (your wish #4).
  local content="rootdir"
  [[ "$class" == "template" ]] && content="vztmpl"
  local count
  count=$(pvesm status -content "$content" | awk 'NR>1{print $1}' | wc -l)

  if [[ "$count" -eq 1 ]]; then
    STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}')
    STORAGE_INFO=""

    # Validate storage space for auto-picked container storage
    if [[ "$class" == "container" && -n "${DISK_SIZE:-}" ]]; then
      validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
      # Continue even if validation fails - user was warned
    fi
  else
    # If the current value is preselectable, we could show it, but per your requirement we always offer selection
    select_storage "$class" || return 150
  fi

  _write_storage_to_vars "$vf" "$key" "$STORAGE_RESULT"

  # Keep environment in sync for later steps (e.g. app-default save)
  if [[ "$class" == "container" ]]; then
    export var_container_storage="$STORAGE_RESULT"
    export CONTAINER_STORAGE="$STORAGE_RESULT"
  else
    export var_template_storage="$STORAGE_RESULT"
    export TEMPLATE_STORAGE="$STORAGE_RESULT"
  fi

  # Silent operation - no output message
}

# ==============================================================================
# SECTION 5: CONFIGURATION & DEFAULTS MANAGEMENT
# ==============================================================================

# ------------------------------------------------------------------------------
# base_settings()
#
# - Defines all base/default variables for container creation
# - Reads from environment variables (var_*)
# - Provides fallback defaults for OS type/version
# - App-specific values take precedence when they are HIGHER (for CPU, RAM, DISK)
# - Sets up container type, resources, network, SSH, features, and tags
# ------------------------------------------------------------------------------
base_settings() {
  # Default Settings
  CT_TYPE=${var_unprivileged:-"1"}

  # Resource allocation: App defaults take precedence if HIGHER
  # Compare app-declared values (saved in APP_DEFAULT_*) with current var_* values
  local final_disk="${var_disk:-4}"
  local final_cpu="${var_cpu:-1}"
  local final_ram="${var_ram:-1024}"

  # If app declared higher values, use those instead
  if [[ -n "${APP_DEFAULT_DISK:-}" && "${APP_DEFAULT_DISK}" =~ ^[0-9]+$ ]]; then
    if [[ "${APP_DEFAULT_DISK}" -gt "${final_disk}" ]]; then
      final_disk="${APP_DEFAULT_DISK}"
    fi
  fi

  if [[ -n "${APP_DEFAULT_CPU:-}" && "${APP_DEFAULT_CPU}" =~ ^[0-9]+$ ]]; then
    if [[ "${APP_DEFAULT_CPU}" -gt "${final_cpu}" ]]; then
      final_cpu="${APP_DEFAULT_CPU}"
    fi
  fi

  if [[ -n "${APP_DEFAULT_RAM:-}" && "${APP_DEFAULT_RAM}" =~ ^[0-9]+$ ]]; then
    if [[ "${APP_DEFAULT_RAM}" -gt "${final_ram}" ]]; then
      final_ram="${APP_DEFAULT_RAM}"
    fi
  fi

  DISK_SIZE="${final_disk}"
  CORE_COUNT="${final_cpu}"
  RAM_SIZE="${final_ram}"
  VERBOSE=${var_verbose:-"${1:-no}"}
  PW=""
  if [[ -n "${var_pw:-}" ]]; then
    local _pw_raw="${var_pw}"
    case "$_pw_raw" in
    --password\ *) _pw_raw="${_pw_raw#--password }" ;;
    -password\ *) _pw_raw="${_pw_raw#-password }" ;;
    esac
    while [[ "$_pw_raw" == -* ]]; do
      _pw_raw="${_pw_raw#-}"
    done
    if [[ -z "$_pw_raw" ]]; then
      msg_warn "Password was only dashes after cleanup; leaving empty."
    else
      PW="--password $_pw_raw"
    fi
  fi

  # Validate and set Container ID
  local requested_id="${var_ctid:-$NEXTID}"
  if ! validate_container_id "$requested_id"; then
    # Only show warning if user manually specified an ID (not auto-assigned)
    if [[ -n "${var_ctid:-}" ]]; then
      msg_warn "Container ID $requested_id is already in use. Using next available ID: $(get_valid_container_id "$requested_id")"
    fi
    requested_id=$(get_valid_container_id "$requested_id")
  fi
  CT_ID="$requested_id"

  # Validate and set Hostname/FQDN
  local requested_hostname="${var_hostname:-$NSAPP}"
  requested_hostname=$(echo "${requested_hostname,,}" | tr -d ' ')
  if ! validate_hostname "$requested_hostname"; then
    if [[ -n "${var_hostname:-}" ]]; then
      msg_warn "Invalid hostname '$requested_hostname'. Using default: $NSAPP"
    fi
    requested_hostname="$NSAPP"
  fi
  HN="$requested_hostname"

  BRG=${var_brg:-"vmbr0"}
  NET=${var_net:-"dhcp"}

  # Resolve IP range if NET contains a range (e.g., 192.168.1.100/24-192.168.1.200/24)
  if is_ip_range "$NET"; then
    msg_info "Scanning IP range: $NET"
    if resolve_ip_from_range "$NET"; then
      NET="$NET_RESOLVED"
    else
      msg_error "Could not find free IP in range. Falling back to DHCP."
      NET="dhcp"
    fi
  fi

  IPV6_METHOD=${var_ipv6_method:-"none"}
  IPV6_STATIC=${var_ipv6_static:-""}
  GATE=${var_gateway:-""}
  APT_CACHER=${var_apt_cacher:-""}
  APT_CACHER_IP=${var_apt_cacher_ip:-""}

  # Runtime check: Verify APT cacher is reachable if configured
  if [[ -n "$APT_CACHER_IP" && "$APT_CACHER" == "yes" ]]; then
    local _check_host _check_port _check_url
    _check_host=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1)
    _check_port=$(echo "$APT_CACHER_IP" | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2)
    if [[ "$APT_CACHER_IP" =~ ^https?:// ]]; then
      _check_url="$APT_CACHER_IP"
      _check_port="${_check_port:-80}"
    else
      _check_port="${_check_port:-3142}"
      _check_url="http://${APT_CACHER_IP}:${_check_port}"
    fi
    if ! curl -s --connect-timeout 2 "${_check_url}" >/dev/null 2>&1; then
      msg_warn "APT Cacher configured but not reachable at ${_check_url}"
      msg_custom "⚠️" "${YW}" "Disabling APT Cacher for this installation"
      APT_CACHER=""
      APT_CACHER_IP=""
    else
      msg_ok "APT Cacher verified at ${_check_url}"
    fi
  fi

  MTU=${var_mtu:-""}
  _sd_val="${var_searchdomain:-""}"
  [[ -n "$_sd_val" ]] && SD="-searchdomain=$_sd_val" || SD=""
  _ns_val="${var_ns:-""}"
  [[ -n "$_ns_val" ]] && NS="-nameserver=$_ns_val" || NS=""
  MAC=${var_mac:-""}
  VLAN=${var_vlan:-""}
  SSH=${var_ssh:-"no"}
  SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
  UDHCPC_FIX=${var_udhcpc_fix:-""}
  TAGS="community-script,${var_tags:-}"
  ENABLE_FUSE=${var_fuse:-"${1:-no}"}
  ENABLE_TUN=${var_tun:-"${1:-no}"}

  # Additional settings that may be skipped if advanced_settings is not run (e.g., App Defaults)
  ENABLE_GPU=${var_gpu:-"no"}
  ENABLE_NESTING=${var_nesting:-"1"}
  ENABLE_KEYCTL=${var_keyctl:-"0"}
  ENABLE_MKNOD=${var_mknod:-"0"}
  ALLOW_MOUNT_FS=${var_mount_fs:-""}
  PROTECT_CT=${var_protection:-"no"}
  CT_TIMEZONE=${var_timezone:-"$timezone"}
  [[ "${CT_TIMEZONE:-}" == Etc/* ]] && CT_TIMEZONE="host" # pct doesn't accept Etc/* zones

  # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
  if [ -z "$var_os" ]; then
    var_os="debian"
  fi
  if [ -z "$var_version" ]; then
    var_version="12"
  fi
}

# ------------------------------------------------------------------------------
# load_vars_file()
#
# - Safe parser for KEY=VALUE lines from vars files
# - Used by default_var_settings and app defaults loading
# - Only loads whitelisted var_* keys
# - Optional force parameter to override existing values (for app defaults)
# ------------------------------------------------------------------------------
load_vars_file() {
  local file="$1"
  local force="${2:-no}" # If "yes", override existing variables
  [ -f "$file" ] || return 0
  msg_info "Loading defaults from ${file}"

  # Allowed var_* keys
  local VAR_WHITELIST=(
    var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_keyctl
    var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
    var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
    var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
  )

  # Whitelist check helper
  _is_whitelisted() {
    local k="$1" w
    for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
    return 1
  }

  local line key val
  while IFS= read -r line || [ -n "$line" ]; do
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    [[ -z "$line" || "$line" == \#* ]] && continue
    if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
      local var_key="${BASH_REMATCH[1]}"
      local var_val="${BASH_REMATCH[2]}"

      [[ "$var_key" != var_* ]] && continue
      _is_whitelisted "$var_key" || continue

      # Strip inline comments (anything after unquoted #)
      # Only strip if not inside quotes
      if [[ ! "$var_val" =~ ^[\"\'] ]]; then
        var_val="${var_val%%#*}"
      fi

      # Strip quotes
      if [[ "$var_val" =~ ^\"(.*)\"$ ]]; then
        var_val="${BASH_REMATCH[1]}"
      elif [[ "$var_val" =~ ^\'(.*)\'$ ]]; then
        var_val="${BASH_REMATCH[1]}"
      fi

      # Trim trailing whitespace
      var_val="${var_val%"${var_val##*[![:space:]]}"}"

      # Validate values before setting (skip empty values - they use defaults)
      if [[ -n "$var_val" ]]; then
        case "$var_key" in
        var_mac)
          if ! validate_mac_address "$var_val"; then
            msg_warn "Invalid MAC address '$var_val' in $file, ignoring"
            continue
          fi
          ;;
        var_vlan)
          if ! validate_vlan_tag "$var_val"; then
            msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring"
            continue
          fi
          ;;
        var_mtu)
          if ! validate_mtu "$var_val"; then
            msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring"
            continue
          fi
          ;;
        var_tags)
          if ! validate_tags "$var_val"; then
            msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring"
            continue
          fi
          ;;
        var_timezone)
          if ! validate_timezone "$var_val"; then
            msg_warn "Invalid timezone '$var_val' in $file, ignoring"
            continue
          fi
          ;;
        var_brg)
          if ! validate_bridge "$var_val"; then
            msg_warn "Bridge '$var_val' not found in $file, ignoring"
            continue
          fi
          ;;
        var_gateway)
          if ! validate_gateway_ip "$var_val"; then
            msg_warn "Invalid gateway IP '$var_val' in $file, ignoring"
            continue
          fi
          ;;
        var_hostname)
          if ! validate_hostname "$var_val"; then
            msg_warn "Invalid hostname '$var_val' in $file, ignoring"
            continue
          fi
          ;;
        var_cpu)
          if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then
            msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring"
            continue
          fi
          ;;
        var_ram)
          if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then
            msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring"
            continue
          fi
          ;;
        var_disk)
          if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then
            msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring"
            continue
          fi
          ;;
        var_unprivileged)
          if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
            msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring"
            continue
          fi
          ;;
        var_nesting)
          if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
            msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring"
            continue
          fi
          # Warn about potential issues with systemd-based OS when nesting is disabled via vars file
          if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then
            msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation"
          fi
          ;;
        var_keyctl)
          if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
            msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring"
            continue
          fi
          ;;
        var_net)
          # var_net can be: dhcp, static IP/CIDR, or IP range
          if [[ "$var_val" != "dhcp" ]]; then
            if is_ip_range "$var_val"; then
              : # IP range is valid, will be resolved at runtime
            elif ! validate_ip_address "$var_val"; then
              msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring"
              continue
            fi
          fi
          ;;
        var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection)
          if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then
            msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring"
            continue
          fi
          ;;
        var_mknod)
          if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
            msg_warn "Invalid mknod value '$var_val' in $file (must be 0 or 1), ignoring"
            continue
          fi
          ;;
        var_mount_fs)
          if [[ ! "$var_val" =~ ^[a-zA-Z0-9,]+$ ]]; then
            msg_warn "Invalid mount_fs value '$var_val' in $file (comma-separated fs names only, e.g. nfs,cifs), ignoring"
            continue
          fi
          ;;
        var_ipv6_method)
          if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then
            msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring"
            continue
          fi
          ;;
        var_apt_cacher_ip)
          # Allow: plain IP/hostname, http://host, https://host:port
          if [[ -n "$var_val" ]] && ! [[ "$var_val" =~ ^(https?://)?[a-zA-Z0-9._-]+(:[0-9]+)?(/.*)?$ ]]; then
            msg_warn "Invalid APT Cacher address '$var_val' in $file, ignoring"
            continue
          fi
          ;;
        var_container_storage | var_template_storage)
          # Validate that the storage exists and is active on the current node
          local _storage_status
          _storage_status=$(pvesm status 2>/dev/null | awk -v s="$var_val" '$1 == s { print $3 }')
          if [[ -z "$_storage_status" ]]; then
            msg_warn "Storage '$var_val' from $file not found on this node, ignoring"
            continue
          elif [[ "$_storage_status" == "disabled" ]]; then
            msg_warn "Storage '$var_val' from $file is disabled on this node, ignoring"
            continue
          fi
          ;;
        esac
      fi

      # Set variable: force mode overrides existing, otherwise only set if empty
      if [[ "$force" == "yes" ]]; then
        export "${var_key}=${var_val}"
      else
        [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}"
      fi
    fi
  done <"$file"
  msg_ok "Loaded ${file}"
}

# ------------------------------------------------------------------------------
# default_var_settings
#
# - Ensures /usr/local/community-scripts/default.vars exists (creates if missing)
# - Loads var_* values from default.vars (safe parser, no source/eval)
# - Precedence: ENV var_* > default.vars > built-in defaults
# - Maps var_verbose → VERBOSE
# - Calls base_settings "$VERBOSE" and echo_default
# ------------------------------------------------------------------------------
default_var_settings() {
  # Allowed var_* keys (alphabetically sorted)
  # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
  local VAR_WHITELIST=(
    var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_keyctl
    var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
    var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
    var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage
  )

  # Snapshot: environment variables (highest precedence)
  declare -A _HARD_ENV=()
  local _k
  for _k in "${VAR_WHITELIST[@]}"; do
    if printenv "$_k" >/dev/null 2>&1; then _HARD_ENV["$_k"]=1; fi
  done

  # Find default.vars location
  local _find_default_vars
  _find_default_vars() {
    local f
    for f in \
      /usr/local/community-scripts/default.vars \
      "$HOME/.config/community-scripts/default.vars" \
      "./default.vars"; do
      [ -f "$f" ] && {
        echo "$f"
        return 0
      }
    done
    return 252
  }
  # Allow override of storages via env (for non-interactive use cases)
  [ -n "${var_template_storage:-}" ] && TEMPLATE_STORAGE="$var_template_storage"
  [ -n "${var_container_storage:-}" ] && CONTAINER_STORAGE="$var_container_storage"

  # Create once, with storages already selected, no var_ctid/var_hostname lines
  local _ensure_default_vars
  _ensure_default_vars() {
    _find_default_vars >/dev/null 2>&1 && return 0

    local canonical="/usr/local/community-scripts/default.vars"
    # Silent creation - no msg_info output
    mkdir -p /usr/local/community-scripts

    # Pick storages before writing the file (always ask unless only one)
    # Create a minimal temp file to write into
    : >"$canonical"

    # Base content (no var_ctid / var_hostname here)
    cat >"$canonical" <<'EOF'
# Community-Scripts defaults (var_* only). Lines starting with # are comments.
# Precedence: ENV var_* > default.vars > built-ins.
# Keep keys alphabetically sorted.

# Container type
var_unprivileged=1

# Resources
var_cpu=1
var_disk=4
var_ram=1024

# Network
var_brg=vmbr0
var_net=dhcp
var_ipv6_method=none
# var_gateway=
# var_vlan=
# var_mtu=
# var_mac=
# var_ns=

# SSH
var_ssh=no
# var_ssh_authorized_key=

# APT cacher (optional - IP or URL)
# var_apt_cacher=yes
# var_apt_cacher_ip=192.168.1.10
# var_apt_cacher_ip=http://proxy.local
# var_apt_cacher_ip=https://proxy.local:443

# Features/Tags/verbosity
var_fuse=no
var_tun=no

# Advanced Settings (Proxmox-official features)
var_nesting=1          # Allow nesting (required for Docker/LXC in CT)
var_keyctl=0           # Allow keyctl() - needed for Docker (systemd-networkd workaround)
var_mknod=0            # Allow device node creation (requires kernel 5.3+, experimental)
var_mount_fs=          # Allow specific filesystems: nfs,fuse,ext4,etc (leave empty for defaults)
var_protection=no      # Prevent accidental deletion of container
var_timezone=          # Container timezone (e.g. Europe/Berlin, leave empty for host timezone)
var_tags=community-script
var_verbose=no

# Security (root PW) – empty => autologin
# var_pw=

# GitHub Personal Access Token (optional – avoids API rate limits during installs)
# Create at https://github.com/settings/tokens – read-only public access is sufficient
# var_github_token=ghp_your_token_here
EOF

    # Now choose storages (always prompt unless just one exists)
    choose_and_set_storage_for_file "$canonical" template
    choose_and_set_storage_for_file "$canonical" container

    chmod 0644 "$canonical"
    # Silent creation - no output message
  }

  # Whitelist check
  local _is_whitelisted_key
  _is_whitelisted_key() {
    local k="$1"
    local w
    for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
    return 1
  }

  # 1) Ensure file exists
  _ensure_default_vars

  # 2) Load file
  local dv
  dv="$(_find_default_vars)" || {
    msg_error "default.vars not found after ensure step"
    return 252
  }
  load_vars_file "$dv"

  # 3) Map var_verbose → VERBOSE
  if [[ -n "${var_verbose:-}" ]]; then
    case "${var_verbose,,}" in 1 | yes | true | on) VERBOSE="yes" ;; 0 | no | false | off) VERBOSE="no" ;; *) VERBOSE="${var_verbose}" ;; esac
  else
    VERBOSE="no"
  fi

  # 4) Map var_github_token → GITHUB_TOKEN (only if not already set in environment)
  if [[ -z "${GITHUB_TOKEN:-}" && -n "${var_github_token:-}" ]]; then
    export GITHUB_TOKEN="${var_github_token}"
  fi

  # 4) Apply base settings and show summary
  METHOD="mydefaults-global"
  base_settings "$VERBOSE"
  header_info
  echo -e "${DEFAULT}${BOLD}${BL}Using User Defaults (default.vars) on node $PVEHOST_NAME${CL}"
  echo_default
}

# ------------------------------------------------------------------------------
# get_app_defaults_path()
#
# - Returns full path for app-specific defaults file
# - Example: /usr/local/community-scripts/defaults/<app>.vars
# ------------------------------------------------------------------------------

get_app_defaults_path() {
  local n="${NSAPP:-${APP,,}}"
  echo "/usr/local/community-scripts/defaults/${n}.vars"
}

# ------------------------------------------------------------------------------
# maybe_offer_save_app_defaults
#
# - Called after advanced_settings returned with fully chosen values.
# - If no <nsapp>.vars exists, offers to persist current advanced settings
#   into /usr/local/community-scripts/defaults/<nsapp>.vars
# - Only writes whitelisted var_* keys.
# - Extracts raw values from flags like ",gw=..." ",mtu=..." etc.
# ------------------------------------------------------------------------------
if ! declare -p VAR_WHITELIST >/dev/null 2>&1; then
  # Note: Removed var_ctid (can only exist once), var_ipv6_static (static IPs are unique)
  declare -ag VAR_WHITELIST=(
    var_apt_cacher var_apt_cacher_ip var_brg var_cpu var_disk var_fuse var_github_token var_gpu var_keyctl
    var_gateway var_hostname var_ipv6_method var_mac var_mknod var_mount_fs var_mtu
    var_net var_nesting var_ns var_os var_protection var_pw var_ram var_tags var_timezone var_tun var_unprivileged
    var_verbose var_version var_vlan var_ssh var_ssh_authorized_key var_container_storage var_template_storage var_searchdomain
  )
fi

# Global whitelist check function (used by _load_vars_file_to_map and others)
_is_whitelisted_key() {
  local k="$1"
  local w
  for w in "${VAR_WHITELIST[@]}"; do [ "$k" = "$w" ] && return 0; done
  return 1
}

_sanitize_value() {
  # Disallow Command-Substitution / Shell-Meta
  case "$1" in
  *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
    echo ""
    return 0
    ;;
  esac
  echo "$1"
}

# Map-Parser: read var_* from file into _VARS_IN associative array
# Note: Main _load_vars_file() with full validation is defined in default_var_settings section
# This simplified version is used specifically for diff operations via _VARS_IN array
declare -A _VARS_IN
_load_vars_file_to_map() {
  local file="$1"
  [ -f "$file" ] || return 0
  _VARS_IN=() # Clear array
  local line key val
  while IFS= read -r line || [ -n "$line" ]; do
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    [ -z "$line" ] && continue
    case "$line" in
    \#*) continue ;;
    esac
    key=$(printf "%s" "$line" | cut -d= -f1)
    val=$(printf "%s" "$line" | cut -d= -f2-)
    case "$key" in
    var_*)
      if _is_whitelisted_key "$key"; then
        _VARS_IN["$key"]="$val"
      fi
      ;;
    esac
  done <"$file"
}

# Diff function for two var_* files -> produces human-readable diff list for $1 (old) vs $2 (new)
_build_vars_diff() {
  local oldf="$1" newf="$2"
  local k
  local -A OLD=() NEW=()
  _load_vars_file_to_map "$oldf"
  for k in "${!_VARS_IN[@]}"; do OLD["$k"]="${_VARS_IN[$k]}"; done
  _load_vars_file_to_map "$newf"
  for k in "${!_VARS_IN[@]}"; do NEW["$k"]="${_VARS_IN[$k]}"; done

  local out
  out+="# Diff for ${APP} (${NSAPP})\n"
  out+="# Old: ${oldf}\n# New: ${newf}\n\n"

  local found_change=0

  # Changed & Removed
  for k in "${!OLD[@]}"; do
    if [[ -v NEW["$k"] ]]; then
      if [[ "${OLD[$k]}" != "${NEW[$k]}" ]]; then
        out+="~ ${k}\n    - old: ${OLD[$k]}\n    + new: ${NEW[$k]}\n"
        found_change=1
      fi
    else
      out+="- ${k}\n    - old: ${OLD[$k]}\n"
      found_change=1
    fi
  done

  # Added
  for k in "${!NEW[@]}"; do
    if [[ ! -v OLD["$k"] ]]; then
      out+="+ ${k}\n    + new: ${NEW[$k]}\n"
      found_change=1
    fi
  done

  if [[ $found_change -eq 0 ]]; then
    out+="(No differences)\n"
  fi

  printf "%b" "$out"
}

# Build a temporary <app>.vars file from current advanced settings
_build_current_app_vars_tmp() {
  tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"

  # NET/GW
  _net="${NET:-}"
  _gate=""
  case "${GATE:-}" in
  ,gw=*) _gate=$(echo "$GATE" | sed 's/^,gw=//') ;;
  esac

  # IPv6
  _ipv6_method="${IPV6_METHOD:-auto}"
  _ipv6_static=""
  _ipv6_gateway=""
  if [ "$_ipv6_method" = "static" ]; then
    _ipv6_static="${IPV6_ADDR:-}"
    _ipv6_gateway="${IPV6_GATE:-}"
  fi

  # MTU/VLAN/MAC
  _mtu=""
  _vlan=""
  _mac=""
  case "${MTU:-}" in
  ,mtu=*) _mtu=$(echo "$MTU" | sed 's/^,mtu=//') ;;
  esac
  case "${VLAN:-}" in
  ,tag=*) _vlan=$(echo "$VLAN" | sed 's/^,tag=//') ;;
  esac
  case "${MAC:-}" in
  ,hwaddr=*) _mac=$(echo "$MAC" | sed 's/^,hwaddr=//') ;;
  esac

  # DNS / Searchdomain
  _ns=""
  _searchdomain=""
  case "${NS:-}" in
  -nameserver=*) _ns=$(echo "$NS" | sed 's/^-nameserver=//') ;;
  esac
  case "${SD:-}" in
  -searchdomain=*) _searchdomain=$(echo "$SD" | sed 's/^-searchdomain=//') ;;
  esac

  # SSH / APT / Features
  _ssh="${SSH:-no}"
  _ssh_auth="${SSH_AUTHORIZED_KEY:-}"
  _apt_cacher="${APT_CACHER:-}"
  _apt_cacher_ip="${APT_CACHER_IP:-}"
  _fuse="${ENABLE_FUSE:-no}"
  _tun="${ENABLE_TUN:-no}"
  _gpu="${ENABLE_GPU:-no}"
  _nesting="${ENABLE_NESTING:-1}"
  _keyctl="${ENABLE_KEYCTL:-0}"
  _mknod="${ENABLE_MKNOD:-0}"
  _mount_fs="${ALLOW_MOUNT_FS:-}"
  _protect="${PROTECT_CT:-no}"
  _timezone="${CT_TIMEZONE:-}"
  _tags="${TAGS:-}"
  _verbose="${VERBOSE:-no}"

  # Type / Resources / Identity
  _unpriv="${CT_TYPE:-1}"
  _cpu="${CORE_COUNT:-1}"
  _ram="${RAM_SIZE:-1024}"
  _disk="${DISK_SIZE:-4}"
  _hostname="${HN:-$NSAPP}"

  # Storage
  _tpl_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}"
  _ct_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}"

  {
    echo "# App-specific defaults for ${APP} (${NSAPP})"
    echo "# Generated on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
    echo

    echo "var_os=$(_sanitize_value "${var_os:-}")"
    echo "var_version=$(_sanitize_value "${var_version:-}")"
    echo "var_unprivileged=$(_sanitize_value "$_unpriv")"
    echo "var_cpu=$(_sanitize_value "$_cpu")"
    echo "var_ram=$(_sanitize_value "$_ram")"
    echo "var_disk=$(_sanitize_value "$_disk")"

    [ -n "${BRG:-}" ] && echo "var_brg=$(_sanitize_value "$BRG")"
    [ -n "$_net" ] && echo "var_net=$(_sanitize_value "$_net")"
    [ -n "$_gate" ] && echo "var_gateway=$(_sanitize_value "$_gate")"
    [ -n "$_mtu" ] && echo "var_mtu=$(_sanitize_value "$_mtu")"
    [ -n "$_vlan" ] && echo "var_vlan=$(_sanitize_value "$_vlan")"
    [ -n "$_mac" ] && echo "var_mac=$(_sanitize_value "$_mac")"
    [ -n "$_ns" ] && echo "var_ns=$(_sanitize_value "$_ns")"

    [ -n "$_ipv6_method" ] && echo "var_ipv6_method=$(_sanitize_value "$_ipv6_method")"
    # var_ipv6_static removed - static IPs are unique, can't be default

    [ -n "$_ssh" ] && echo "var_ssh=$(_sanitize_value "$_ssh")"
    [ -n "$_ssh_auth" ] && echo "var_ssh_authorized_key=$(_sanitize_value "$_ssh_auth")"

    [ -n "$_apt_cacher" ] && echo "var_apt_cacher=$(_sanitize_value "$_apt_cacher")"
    [ -n "$_apt_cacher_ip" ] && echo "var_apt_cacher_ip=$(_sanitize_value "$_apt_cacher_ip")"

    [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")"
    [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")"
    [ -n "$_gpu" ] && echo "var_gpu=$(_sanitize_value "$_gpu")"
    [ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")"
    [ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")"
    [ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")"
    [ -n "$_mount_fs" ] && echo "var_mount_fs=$(_sanitize_value "$_mount_fs")"
    [ -n "$_protect" ] && echo "var_protection=$(_sanitize_value "$_protect")"
    [ -n "$_timezone" ] && echo "var_timezone=$(_sanitize_value "$_timezone")"
    [ -n "$_tags" ] && echo "var_tags=$(_sanitize_value "$_tags")"
    [ -n "$_verbose" ] && echo "var_verbose=$(_sanitize_value "$_verbose")"

    [ -n "$_hostname" ] && echo "var_hostname=$(_sanitize_value "$_hostname")"
    [ -n "$_searchdomain" ] && echo "var_searchdomain=$(_sanitize_value "$_searchdomain")"

    [ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
    [ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
  } >"$tmpf"

  echo "$tmpf"
}

# ------------------------------------------------------------------------------
# maybe_offer_save_app_defaults()
#
# - Called after advanced_settings()
# - Offers to save current values as app defaults if not existing
# - If file exists: shows diff and allows Update, Keep, View Diff, or Cancel
# ------------------------------------------------------------------------------
maybe_offer_save_app_defaults() {
  local app_vars_path
  app_vars_path="$(get_app_defaults_path)"

  # always build from current settings
  local new_tmp diff_tmp
  new_tmp="$(_build_current_app_vars_tmp)"
  diff_tmp="$(mktemp -p /tmp "${NSAPP:-app}.vars.diff.XXXXXX")"

  # 1) if no file → offer to create
  if [[ ! -f "$app_vars_path" ]]; then
    if whiptail --backtitle "Proxmox VE Helper Scripts" \
      --yesno "Save these advanced settings as defaults for ${APP}?\n\nThis will create:\n${app_vars_path}" 12 72; then
      mkdir -p "$(dirname "$app_vars_path")"
      /usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
      msg_ok "Saved app defaults: ${app_vars_path}"
    fi
    rm -f "$new_tmp" "$diff_tmp"
    return 0
  fi

  # 2) if file exists → build diff
  _build_vars_diff "$app_vars_path" "$new_tmp" >"$diff_tmp"

  # if no differences → do nothing
  if grep -q "^(No differences)$" "$diff_tmp"; then
    rm -f "$new_tmp" "$diff_tmp"
    return 0
  fi

  # 3) if file exists → show menu with default selection "Update Defaults"
  local app_vars_file
  app_vars_file="$(basename "$app_vars_path")"

  while true; do
    local sel
    sel="$(whiptail --backtitle "Proxmox VE Helper Scripts" \
      --title "APP DEFAULTS – ${APP}" \
      --menu "Differences detected. What do you want to do?" 20 78 10 \
      "Update Defaults" "Write new values to ${app_vars_file}" \
      "Keep Current" "Keep existing defaults (no changes)" \
      "View Diff" "Show a detailed diff" \
      "Cancel" "Abort without changes" \
      --default-item "Update Defaults" \
      3>&1 1>&2 2>&3)" || { sel="Cancel"; }

    case "$sel" in
    "Update Defaults")
      /usr/bin/install -m 0644 "$new_tmp" "$app_vars_path"
      msg_ok "Updated app defaults: ${app_vars_path}"
      break
      ;;
    "Keep Current")
      msg_custom "ℹ️" "${BL}" "Keeping current app defaults: ${app_vars_path}"
      break
      ;;
    "View Diff")
      whiptail --backtitle "Proxmox VE Helper Scripts" \
        --title "Diff – ${APP}" \
        --scrolltext --textbox "$diff_tmp" 25 100
      ;;
    "Cancel" | *)
      msg_custom "🚫" "${YW}" "Canceled. No changes to app defaults."
      break
      ;;
    esac
  done

  rm -f "$new_tmp" "$diff_tmp"
}

ensure_storage_selection_for_vars_file() {
  local vf="$1"

  # Read stored values (if any)
  local tpl ct
  tpl=$(grep -E '^var_template_storage=' "$vf" | cut -d= -f2- || true)
  ct=$(grep -E '^var_container_storage=' "$vf" | cut -d= -f2- || true)

  if [[ -n "$tpl" && -n "$ct" ]]; then
    TEMPLATE_STORAGE="$tpl"
    CONTAINER_STORAGE="$ct"

    # Validate storage space for loaded container storage
    if [[ -n "${DISK_SIZE:-}" ]]; then
      validate_storage_space "$ct" "$DISK_SIZE" "yes"
      # Continue even if validation fails - user was warned
    fi

    return 0
  fi

  choose_and_set_storage_for_file "$vf" template
  choose_and_set_storage_for_file "$vf" container

  # Silent operation - no output message
}

ensure_global_default_vars_file() {
  local vars_path="/usr/local/community-scripts/default.vars"
  if [[ ! -f "$vars_path" ]]; then
    mkdir -p "$(dirname "$vars_path")"
    touch "$vars_path"
  fi
  echo "$vars_path"
}

# ==============================================================================
# SECTION 6: ADVANCED INTERACTIVE CONFIGURATION
# ==============================================================================

# ------------------------------------------------------------------------------
# advanced_settings()
#
# - Interactive wizard-style configuration with BACK navigation
# - State-machine approach: each step can go forward or backward
# - Cancel at Step 1 = Exit Script, Cancel at other steps = Go Back
# - Allows user to customize all container settings
# ------------------------------------------------------------------------------
advanced_settings() {
  # Enter alternate screen buffer to prevent flicker between dialogs
  tput smcup 2>/dev/null || true
  trap 'tput rmcup 2>/dev/null || true' RETURN

  # Initialize defaults
  TAGS="community-script;${var_tags:-}"
  local STEP=1
  local MAX_STEP=28

  # Store values for back navigation - inherit from var_* app defaults
  local _ct_type="${var_unprivileged:-1}"
  local _pw=""
  local _pw_display="Automatic Login"
  local _ct_id="$NEXTID"
  local _hostname="$NSAPP"
  local _disk_size="${var_disk:-4}"
  local _core_count="${var_cpu:-1}"
  local _ram_size="${var_ram:-1024}"
  local _bridge="${var_brg:-vmbr0}"
  local _net="${var_net:-dhcp}"
  local _gate="${var_gateway:-}"
  local _ipv6_method="${var_ipv6_method:-auto}"
  local _ipv6_addr=""
  local _ipv6_gate=""
  local _apt_cacher="${var_apt_cacher:-no}"
  local _apt_cacher_ip="${var_apt_cacher_ip:-}"
  local _mtu="${var_mtu:-}"
  local _sd="${var_searchdomain:-}"
  local _ns="${var_ns:-}"
  local _mac="${var_mac:-}"
  local _vlan="${var_vlan:-}"
  local _tags="$TAGS"
  local _enable_fuse="${var_fuse:-no}"
  local _enable_tun="${var_tun:-no}"
  local _enable_gpu="${var_gpu:-no}"
  local _enable_nesting="${var_nesting:-1}"
  local _verbose="${var_verbose:-no}"
  local _enable_keyctl="${var_keyctl:-0}"
  local _enable_mknod="${var_mknod:-0}"
  local _mount_fs="${var_mount_fs:-}"
  local _protect_ct="${var_protection:-no}"

  # Detect host timezone for default (if not set via var_timezone)
  local _host_timezone=""
  if command -v timedatectl >/dev/null 2>&1; then
    _host_timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "")
  elif [ -f /etc/timezone ]; then
    _host_timezone=$(cat /etc/timezone 2>/dev/null || echo "")
  fi
  # Map Etc/* timezones to "host" (pct doesn't accept Etc/* zones)
  [[ "${_host_timezone:-}" == Etc/* ]] && _host_timezone="host"
  local _ct_timezone="${var_timezone:-$_host_timezone}"
  [[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host"

  # Helper to show current progress
  show_progress() {
    local current=$1
    local total=$MAX_STEP
    echo -e "\n${INFO}${BOLD}${DGN}Step $current of $total${CL}"
  }

  # Detect available bridges (do this once)
  local BRIDGES=""
  local BRIDGE_MENU_OPTIONS=()
  _detect_bridges() {
    IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f 2>/dev/null)
    BRIDGES=""
    local OLD_IFS=$IFS
    IFS=$'\n'
    for iface_filepath in ${IFACE_FILEPATH_LIST}; do
      local iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
      (grep -Pn '^\s*iface' "${iface_filepath}" 2>/dev/null | cut -d':' -f1 && wc -l "${iface_filepath}" 2>/dev/null | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" 2>/dev/null || true
      if [ -f "${iface_indexes_tmpfile}" ]; then
        while read -r pair; do
          local start=$(echo "${pair}" | cut -d':' -f1)
          local end=$(echo "${pair}" | cut -d':' -f2)
          if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" 2>/dev/null | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
            local iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
            BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
          fi
        done <"${iface_indexes_tmpfile}"
        rm -f "${iface_indexes_tmpfile}"
      fi
    done
    IFS=$OLD_IFS
    BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)

    # Build bridge menu
    BRIDGE_MENU_OPTIONS=()
    if [[ -n "$BRIDGES" ]]; then
      while IFS= read -r bridge; do
        if [[ -n "$bridge" ]]; then
          local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//' || true)
          BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }")
        fi
      done <<<"$BRIDGES"
    fi
  }
  _detect_bridges

  # Main wizard loop
  while [ $STEP -le $MAX_STEP ]; do
    case $STEP in

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 1: Container Type
    # ═══════════════════════════════════════════════════════════════════════════
    1)
      local default_on="ON"
      local default_off="OFF"
      [[ "$_ct_type" == "0" ]] && {
        default_on="OFF"
        default_off="ON"
      }

      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "CONTAINER TYPE" \
        --ok-button "Next" --cancel-button "Exit" \
        --radiolist "\nChoose container type:\n\nUse SPACE to select, ENTER to confirm." 14 58 2 \
        "1" "Unprivileged (recommended)" $default_on \
        "0" "Privileged" $default_off \
        3>&1 1>&2 2>&3); then
        [[ -n "$result" ]] && _ct_type="$result"
        ((STEP++))
      else
        exit_script
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 2: Root Password
    # ════════════════════════════════════════���═══════════════════════════════���══
    2)
      if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "ROOT PASSWORD" \
        --ok-button "Next" --cancel-button "Back" \
        --passwordbox "\nSet Root Password (needed for root ssh access)\n\nLeave blank for automatic login (no password)" 12 58 \
        3>&1 1>&2 2>&3); then

        if [[ -z "$PW1" ]]; then
          _pw=""
          _pw_display="Automatic Login"
          ((STEP++))
        elif [[ "$PW1" == *" "* ]]; then
          whiptail --msgbox "Password cannot contain spaces." 8 58
        else
          local _pw1_clean="$PW1"
          while [[ "$_pw1_clean" == -* ]]; do
            _pw1_clean="${_pw1_clean#-}"
          done
          if [[ -z "$_pw1_clean" ]]; then
            whiptail --msgbox "Password cannot be only '-' characters." 8 58
            continue
          elif ((${#_pw1_clean} < 5)); then
            whiptail --msgbox "Password must be at least 5 characters (after removing leading '-')." 8 70
            continue
          fi
          # Verify password
          if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
            --title "PASSWORD VERIFICATION" \
            --ok-button "Confirm" --cancel-button "Back" \
            --passwordbox "\nVerify Root Password" 10 58 \
            3>&1 1>&2 2>&3); then
            local _pw2_clean="$PW2"
            while [[ "$_pw2_clean" == -* ]]; do
              _pw2_clean="${_pw2_clean#-}"
            done
            if [[ "$_pw1_clean" == "$_pw2_clean" ]]; then
              _pw="--password $_pw1_clean"
              _pw_display="********"
              ((STEP++))
            else
              whiptail --msgbox "Passwords do not match. Please try again." 8 58
            fi
          else
            ((STEP--))
          fi
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 3: Container ID
    # ═══════════════════════════════════════════════════════════════════════════
    3)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "CONTAINER ID" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet Container ID" 10 58 "$_ct_id" \
        3>&1 1>&2 2>&3); then
        local input_id="${result:-$NEXTID}"

        # Validate that ID is numeric
        if ! [[ "$input_id" =~ ^[0-9]+$ ]]; then
          whiptail --backtitle "Proxmox VE Helper Scripts" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58
          continue
        fi

        # Check if ID is already in use
        if ! validate_container_id "$input_id"; then
          if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" \
            --yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID ($(get_valid_container_id "$input_id"))?" 10 58; then
            _ct_id=$(get_valid_container_id "$input_id")
          else
            continue
          fi
        else
          _ct_id="$input_id"
        fi
        ((STEP++))
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 4: Hostname
    # ═══════════════════════════════════════════════════════════════════════════
    4)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "HOSTNAME" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet Hostname (or FQDN, e.g. host.example.com)" 10 58 "$_hostname" \
        3>&1 1>&2 2>&3); then
        local hn_test="${result:-$NSAPP}"
        hn_test=$(echo "${hn_test,,}" | tr -d ' ')

        if validate_hostname "$hn_test"; then
          _hostname="$hn_test"
          ((STEP++))
        else
          whiptail --msgbox "Invalid hostname: '$hn_test'\n\nRules:\n- Only lowercase letters, digits, dots and hyphens\n- Labels separated by dots (max 63 chars each)\n- No leading/trailing hyphens or dots\n- No consecutive dots\n- Total max 253 characters" 14 60
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 5: Disk Size
    # ═══════════════════════════════════════════════════════════════════════════
    5)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "DISK SIZE" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet Disk Size in GB" 10 58 "$_disk_size" \
        3>&1 1>&2 2>&3); then
        local disk_test="${result:-$var_disk}"
        if [[ "$disk_test" =~ ^[1-9][0-9]*$ ]]; then
          _disk_size="$disk_test"
          ((STEP++))
        else
          whiptail --msgbox "Disk size must be a positive integer!" 8 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 6: CPU Cores
    # ═══════════════════════════════════════════════════════════════════════════
    6)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "CPU CORES" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nAllocate CPU Cores" 10 58 "$_core_count" \
        3>&1 1>&2 2>&3); then
        local cpu_test="${result:-$var_cpu}"
        if [[ "$cpu_test" =~ ^[1-9][0-9]*$ ]]; then
          _core_count="$cpu_test"
          ((STEP++))
        else
          whiptail --msgbox "CPU core count must be a positive integer!" 8 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 7: RAM Size
    # ═══════════════════════════════════════════════════════════════════════════
    7)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "RAM SIZE" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nAllocate RAM in MiB" 10 58 "$_ram_size" \
        3>&1 1>&2 2>&3); then
        local ram_test="${result:-$var_ram}"
        if [[ "$ram_test" =~ ^[1-9][0-9]*$ ]]; then
          _ram_size="$ram_test"
          ((STEP++))
        else
          whiptail --msgbox "RAM size must be a positive integer!" 8 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 8: Network Bridge
    # ═══════════════════════════════════════════════════════════════════════════
    8)
      if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then
        # Validate default bridge exists
        if validate_bridge "vmbr0"; then
          _bridge="vmbr0"
          ((STEP++))
        else
          whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58
          msg_error "Default bridge 'vmbr0' not found"
          exit 116
        fi
      else
        if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
          --title "NETWORK BRIDGE" \
          --ok-button "Next" --cancel-button "Back" \
          --menu "\nSelect network bridge:" 16 58 6 \
          "${BRIDGE_MENU_OPTIONS[@]}" \
          3>&1 1>&2 2>&3); then
          local bridge_test="${result:-vmbr0}"
          # Skip separator entries (e.g., __other__) - re-display menu
          if [[ "$bridge_test" == "__other__" || "$bridge_test" == -* ]]; then
            continue
          fi
          if validate_bridge "$bridge_test"; then
            _bridge="$bridge_test"
            ((STEP++))
          else
            whiptail --msgbox "Bridge '$bridge_test' is not available or not active." 8 58
          fi
        else
          ((STEP--))
        fi
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 9: IPv4 Configuration
    # ═══════════════════════════════════════════════════════════════════════════
    9)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "IPv4 CONFIGURATION" \
        --ok-button "Next" --cancel-button "Back" \
        --menu "\nSelect IPv4 Address Assignment:" 16 65 3 \
        "dhcp" "Automatic (DHCP, recommended)" \
        "static" "Static (manual entry)" \
        "range" "IP Range Scan (find first free IP)" \
        3>&1 1>&2 2>&3); then

        if [[ "$result" == "static" ]]; then
          # Get static IP
          local static_ip
          if static_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
            --title "STATIC IPv4 ADDRESS" \
            --ok-button "Next" --cancel-button "Back" \
            --inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \
            3>&1 1>&2 2>&3); then
            if validate_ip_address "$static_ip"; then
              # Get gateway
              local gateway_ip
              if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
                --title "GATEWAY IP" \
                --ok-button "Next" --cancel-button "Back" \
                --inputbox "\nEnter Gateway IP address" 10 58 "" \
                3>&1 1>&2 2>&3); then
                if validate_gateway_ip "$gateway_ip"; then
                  # Validate gateway is in same subnet
                  if validate_gateway_in_subnet "$static_ip" "$gateway_ip"; then
                    _net="$static_ip"
                    _gate=",gw=$gateway_ip"
                    ((STEP++))
                  else
                    whiptail --msgbox "Gateway is not in the same subnet as the static IP.\n\nStatic IP: $static_ip\nGateway: $gateway_ip" 10 58
                  fi
                else
                  whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
                fi
              fi
            else
              whiptail --msgbox "Invalid IPv4 CIDR format.\n\nEach octet must be 0-255.\nCIDR must be 1-32.\nExample: 192.168.1.100/24" 12 58
            fi
          fi
        elif [[ "$result" == "range" ]]; then
          # IP Range Scan
          local ip_range
          if ip_range=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
            --title "IP RANGE SCAN" \
            --ok-button "Scan" --cancel-button "Back" \
            --inputbox "\nEnter IP range to scan for free address\n(e.g. 192.168.1.100/24-192.168.1.200/24)" 12 65 "" \
            3>&1 1>&2 2>&3); then
            if is_ip_range "$ip_range"; then
              # Exit whiptail screen temporarily to show scan progress
              clear
              header_info
              echo -e "${INFO}${BOLD}${DGN}Scanning IP range for free address...${CL}\n"
              if resolve_ip_from_range "$ip_range"; then
                # Get gateway
                local gateway_ip
                if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
                  --title "GATEWAY IP" \
                  --ok-button "Next" --cancel-button "Back" \
                  --inputbox "\nFound free IP: $NET_RESOLVED\n\nEnter Gateway IP address" 12 58 "" \
                  3>&1 1>&2 2>&3); then
                  if validate_gateway_ip "$gateway_ip"; then
                    # Validate gateway is in same subnet
                    if validate_gateway_in_subnet "$NET_RESOLVED" "$gateway_ip"; then
                      _net="$NET_RESOLVED"
                      _gate=",gw=$gateway_ip"
                      ((STEP++))
                    else
                      whiptail --msgbox "Gateway is not in the same subnet as the IP.\n\nIP: $NET_RESOLVED\nGateway: $gateway_ip" 10 58
                    fi
                  else
                    whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58
                  fi
                fi
              else
                whiptail --msgbox "No free IP found in the specified range.\nAll IPs responded to ping." 10 58
              fi
            else
              whiptail --msgbox "Invalid IP range format.\n\nExample: 192.168.1.100/24-192.168.1.200/24" 10 58
            fi
          fi
        else
          _net="dhcp"
          _gate=""
          ((STEP++))
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 10: IPv6 Configuration
    # ═══════════════════════════════════════════════════════════════════════════
    10)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "IPv6 CONFIGURATION" \
        --ok-button "Next" --cancel-button "Back" \
        --menu "\nSelect IPv6 Address Management:" 16 70 5 \
        "auto" "SLAAC/AUTO (recommended) - Dynamic IPv6 from network" \
        "dhcp" "DHCPv6 - DHCP-assigned IPv6 address" \
        "static" "Static - Manual IPv6 address configuration" \
        "none" "None - No IPv6 assignment (most containers)" \
        "disable" "Fully Disabled - (breaks some services)" \
        3>&1 1>&2 2>&3); then

        _ipv6_method="$result"
        case "$result" in
        static)
          local ipv6_addr
          if ipv6_addr=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
            --title "STATIC IPv6 ADDRESS" \
            --inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \
            3>&1 1>&2 2>&3); then
            if validate_ipv6_address "$ipv6_addr"; then
              _ipv6_addr="$ipv6_addr"
              # Optional gateway - loop until valid or empty
              local ipv6_gw_valid=false
              while [[ "$ipv6_gw_valid" == "false" ]]; do
                local ipv6_gw
                ipv6_gw=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
                  --title "IPv6 GATEWAY" \
                  --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \
                  3>&1 1>&2 2>&3) || true
                # Validate gateway if provided
                if [[ -n "$ipv6_gw" ]]; then
                  if validate_ipv6_address "$ipv6_gw"; then
                    _ipv6_gate="$ipv6_gw"
                    ipv6_gw_valid=true
                    ((STEP++))
                  else
                    whiptail --msgbox "Invalid IPv6 gateway format.\n\nExample: 2001:db8::1" 8 58
                  fi
                else
                  _ipv6_gate=""
                  ipv6_gw_valid=true
                  ((STEP++))
                fi
              done
            else
              whiptail --msgbox "Invalid IPv6 CIDR format.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 58
            fi
          fi
          ;;
        dhcp)
          _ipv6_addr="dhcp"
          _ipv6_gate=""
          ((STEP++))
          ;;

        none)
          _ipv6_addr="none"
          _ipv6_gate=""
          ((STEP++))
          ;;
        *)
          _ipv6_addr=""
          _ipv6_gate=""
          ((STEP++))
          ;;
        esac
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 11: MTU Size
    # ═══════════════════════════════════════════════════════════════════════════
    11)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "MTU SIZE" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500, common values: 1500, 9000)" 12 62 "" \
        3>&1 1>&2 2>&3); then
        if validate_mtu "$result"; then
          _mtu="$result"
          ((STEP++))
        else
          whiptail --msgbox "Invalid MTU size.\n\nMTU must be between 576 and 65535.\nCommon values: 1500 (default), 9000 (jumbo frames)" 10 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 12: DNS Search Domain
    # ═══════════════════════════════════════════════════════════════════════════
    12)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "DNS SEARCH DOMAIN" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet DNS Search Domain\n(leave blank to use host setting)" 12 58 "" \
        3>&1 1>&2 2>&3); then
        _sd="$result"
        ((STEP++))
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 13: DNS Server
    # ═══════════════════════════════════════════════════════════════════════════
    13)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "DNS SERVER" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet DNS Server IP\n(leave blank to use host setting)" 12 58 "" \
        3>&1 1>&2 2>&3); then
        _ns="$result"
        ((STEP++))
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 14: MAC Address
    # ═══════════════════════════════════════════════════════════════════════════
    14)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "MAC ADDRESS" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet MAC Address\n(leave blank for auto-generated, format: XX:XX:XX:XX:XX:XX)" 12 62 "" \
        3>&1 1>&2 2>&3); then
        if validate_mac_address "$result"; then
          _mac="$result"
          ((STEP++))
        else
          whiptail --msgbox "Invalid MAC address format.\n\nRequired format: XX:XX:XX:XX:XX:XX\nExample: 02:00:00:00:00:01" 10 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 15: VLAN Tag
    # ═══════════════════════════════════════════════════════════════════════════
    15)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "VLAN TAG" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet VLAN Tag (1-4094)\n(leave blank for no VLAN)" 12 58 "" \
        3>&1 1>&2 2>&3); then
        if validate_vlan_tag "$result"; then
          _vlan="$result"
          ((STEP++))
        else
          whiptail --msgbox "Invalid VLAN tag.\n\nVLAN must be a number between 1 and 4094." 8 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 16: Tags
    # ═══════════════════════════════════════════════════════════════════════════
    16)
      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "CONTAINER TAGS" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet Custom Tags (semicolon-separated)\n(alphanumeric, hyphens, underscores only)" 12 58 "$_tags" \
        3>&1 1>&2 2>&3); then
        local tags_test="${result:-}"
        tags_test=$(echo "$tags_test" | tr -d '[:space:]')
        if validate_tags "$tags_test"; then
          _tags="$tags_test"
          ((STEP++))
        else
          whiptail --msgbox "Invalid tag format.\n\nTags can only contain:\n- Letters (a-z, A-Z)\n- Numbers (0-9)\n- Hyphens (-)\n- Underscores (_)\n- Semicolons (;) as separator" 14 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 17: SSH Settings
    # ═══════════════════════════════════════════════════════════════════════════
    17)
      configure_ssh_settings "Step $STEP/$MAX_STEP"
      # configure_ssh_settings handles its own flow, always advance
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 18: FUSE Support
    # ═══════════════════════════════════════════════════════════════════════════
    18)
      local fuse_default_flag="--defaultno"
      [[ "$_enable_fuse" == "yes" || "$_enable_fuse" == "1" ]] && fuse_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "FUSE SUPPORT" \
        --ok-button "Next" --cancel-button "Back" \
        $fuse_default_flag \
        --yesno "\nEnable FUSE support?\n\nRequired for: rclone, mergerfs, AppImage, etc.\n\n(App default: ${var_fuse:-no})" 14 58; then
        _enable_fuse="yes"
      else
        if [ $? -eq 1 ]; then
          _enable_fuse="no"
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 19: TUN/TAP Support
    # ═══════════════════════════════════════════════════════════════════════════
    19)
      local tun_default_flag="--defaultno"
      [[ "$_enable_tun" == "yes" || "$_enable_tun" == "1" ]] && tun_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "TUN/TAP SUPPORT" \
        --ok-button "Next" --cancel-button "Back" \
        $tun_default_flag \
        --yesno "\nEnable TUN/TAP device support?\n\nRequired for: VPN apps (WireGuard, OpenVPN, Tailscale),\nnetwork tunneling, and containerized networking.\n\n(App default: ${var_tun:-no})" 14 62; then
        _enable_tun="yes"
      else
        if [ $? -eq 1 ]; then
          _enable_tun="no"
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 20: Nesting Support
    # ═══════════════════════════════════════════════════════════════════════════
    20)
      local nesting_default_flag=""
      [[ "$_enable_nesting" == "0" || "$_enable_nesting" == "no" ]] && nesting_default_flag="--defaultno"

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "NESTING SUPPORT" \
        --ok-button "Next" --cancel-button "Back" \
        $nesting_default_flag \
        --yesno "\nEnable Nesting?\n\nRequired for: Docker, LXC inside LXC, Podman,\nand other containerization tools.\n\n(App default: ${var_nesting:-1})" 14 58; then
        _enable_nesting="1"
      else
        if [ $? -eq 1 ]; then
          _enable_nesting="0"
          # Warn about potential issues with systemd-based OS when nesting is disabled
          if [[ "$var_os" != "alpine" ]]; then
            whiptail --backtitle "Proxmox VE Helper Scripts" \
              --title "⚠️ NESTING WARNING" \
              --msgbox "Modern systemd-based distributions (Debian 13+, Ubuntu 24.04+, etc.) may require nesting to be enabled for proper operation.\n\nWithout nesting, the container may start in a degraded state with failing services (error 243/CREDENTIALS).\n\nIf you experience issues, enable nesting in the container options." 14 68
          fi
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 21: GPU Passthrough
    # ═══════════════════════════════════════════════════════════════════════════
    21)
      local gpu_default_flag="--defaultno"
      [[ "$_enable_gpu" == "yes" ]] && gpu_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "GPU PASSTHROUGH" \
        --ok-button "Next" --cancel-button "Back" \
        $gpu_default_flag \
        --yesno "\nEnable GPU Passthrough?\n\nAutomatically detects and passes through available GPUs\n(Intel/AMD/NVIDIA) for hardware acceleration.\n\nRecommended for: Media servers, AI/ML, Transcoding\n\n(App default: ${var_gpu:-no})" 16 62; then
        _enable_gpu="yes"
      else
        if [ $? -eq 1 ]; then
          _enable_gpu="no"
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 22: Keyctl Support (Docker/systemd)
    # ═══════════════════════════════════════════════════════════════════════════
    22)
      local keyctl_default_flag="--defaultno"
      [[ "$_enable_keyctl" == "1" ]] && keyctl_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "KEYCTL SUPPORT" \
        --ok-button "Next" --cancel-button "Back" \
        $keyctl_default_flag \
        --yesno "\nEnable Keyctl support?\n\nRequired for: Docker containers, systemd-networkd,\nand kernel keyring operations.\n\nNote: Automatically enabled for unprivileged containers.\n\n(App default: ${var_keyctl:-0})" 16 62; then
        _enable_keyctl="1"
      else
        if [ $? -eq 1 ]; then
          _enable_keyctl="0"
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 23: APT Cacher Proxy
    # ═══════════════════════════════════════════════════════════════════════════
    23)
      local apt_cacher_default_flag="--defaultno"
      [[ "$_apt_cacher" == "yes" ]] && apt_cacher_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "APT CACHER PROXY" \
        --ok-button "Next" --cancel-button "Back" \
        $apt_cacher_default_flag \
        --yesno "\nUse APT Cacher-NG proxy?\n\nSpeeds up package downloads by caching them locally.\nRequires apt-cacher-ng running on your network.\n\n(App default: ${var_apt_cacher:-no})" 14 62; then
        _apt_cacher="yes"
        # Ask for IP if enabled
        if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
          --title "APT CACHER IP" \
          --inputbox "\nEnter APT Cacher-NG IP or URL:\n(e.g. 192.168.1.10, http://host, https://host:443)" 12 62 "$_apt_cacher_ip" \
          3>&1 1>&2 2>&3); then
          _apt_cacher_ip="$result"
        fi
      else
        if [ $? -eq 1 ]; then
          _apt_cacher="no"
          _apt_cacher_ip=""
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 24: Container Timezone
    # ═══════════════════════════════════════════════════════════════════════════
    24)
      local tz_hint="$_ct_timezone"
      [[ -z "$tz_hint" ]] && tz_hint="(empty - will use host timezone)"

      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "CONTAINER TIMEZONE" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \
        3>&1 1>&2 2>&3); then
        local tz_test="$result"
        [[ "${tz_test:-}" == Etc/* ]] && tz_test="host" # pct doesn't accept Etc/* zones
        if validate_timezone "$tz_test"; then
          _ct_timezone="$tz_test"
          ((STEP++))
        else
          whiptail --msgbox "Invalid timezone: '$result'\n\nTimezone must exist in /usr/share/zoneinfo/\n\nExamples:\n- Europe/Berlin\n- America/New_York\n- Asia/Tokyo\n- UTC" 14 58
        fi
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 25: Container Protection
    # ═══════════════════════════════════════════════════════════════════════════
    25)
      local protect_default_flag="--defaultno"
      [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "CONTAINER PROTECTION" \
        --ok-button "Next" --cancel-button "Back" \
        $protect_default_flag \
        --yesno "\nEnable Container Protection?\n\nPrevents accidental deletion of this container.\nYou must disable protection before removing.\n\n(App default: ${var_protection:-no})" 14 62; then
        _protect_ct="yes"
      else
        if [ $? -eq 1 ]; then
          _protect_ct="no"
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 26: Device Node Creation (mknod)
    # ═══════════════════════════════════════════════════════════════════════════
    26)
      local mknod_default_flag="--defaultno"
      [[ "$_enable_mknod" == "1" ]] && mknod_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "DEVICE NODE CREATION" \
        --ok-button "Next" --cancel-button "Back" \
        $mknod_default_flag \
        --yesno "\nAllow device node creation (mknod)?\n\nRequired for: Creating device files inside container.\nExperimental feature (requires kernel 5.3+).\n\n(App default: ${var_mknod:-0})" 14 62; then
        _enable_mknod="1"
      else
        if [ $? -eq 1 ]; then
          _enable_mknod="0"
        else
          ((STEP--))
          continue
        fi
      fi
      ((STEP++))
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 27: Mount Filesystems
    # ═══════════════════════════════════════════════════════════════════════════
    27)
      local mount_hint=""
      [[ -n "$_mount_fs" ]] && mount_hint="$_mount_fs" || mount_hint="(none)"

      if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "MOUNT FILESYSTEMS" \
        --ok-button "Next" --cancel-button "Back" \
        --inputbox "\nAllow specific filesystem mounts.\n\nComma-separated list: nfs, cifs, fuse, ext4, etc.\nLeave empty for defaults (none).\n\nCurrent: $mount_hint" 14 62 "$_mount_fs" \
        3>&1 1>&2 2>&3); then
        _mount_fs="$result"
        ((STEP++))
      else
        ((STEP--))
      fi
      ;;

    # ═══════════════════════════════════════════════════════════════════════════
    # STEP 28: Verbose Mode & Confirmation
    # ═══════════════════════════════════════════════════════════════════════════
    28)
      local verbose_default_flag="--defaultno"
      [[ "$_verbose" == "yes" ]] && verbose_default_flag=""

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "VERBOSE MODE" \
        $verbose_default_flag \
        --yesno "\nEnable Verbose Mode?\n\nShows detailed output during installation." 12 58; then
        _verbose="yes"
      else
        _verbose="no"
      fi
      # Build summary
      local ct_type_desc="Unprivileged"
      [[ "$_ct_type" == "0" ]] && ct_type_desc="Privileged"

      local nesting_desc="Disabled"
      [[ "$_enable_nesting" == "1" ]] && nesting_desc="Enabled"

      local keyctl_desc="Disabled"
      [[ "$_enable_keyctl" == "1" ]] && keyctl_desc="Enabled"

      local protect_desc="No"
      [[ "$_protect_ct" == "yes" || "$_protect_ct" == "1" ]] && protect_desc="Yes"

      local tz_display="${_ct_timezone:-Host TZ}"
      local apt_display="${_apt_cacher:-no}"
      [[ "$_apt_cacher" == "yes" && -n "$_apt_cacher_ip" ]] && apt_display="$_apt_cacher_ip"

      local summary="Container Type: $ct_type_desc
Container ID: $_ct_id
Hostname: $_hostname

Resources:
  Disk: ${_disk_size} GB
  CPU: $_core_count cores
  RAM: $_ram_size MiB

Network:
  Bridge: $_bridge
  IPv4: $_net
  IPv6: $_ipv6_method

Features:
  FUSE: $_enable_fuse | TUN: $_enable_tun
  Nesting: $nesting_desc | Keyctl: $keyctl_desc
  Mknod: $([ "$_enable_mknod" == "1" ] && echo Enabled || echo Disabled) | Mount FS: ${_mount_fs:-(none)}
  GPU: $_enable_gpu | Protection: $protect_desc

Advanced:
  Timezone: $tz_display
  APT Cacher: $apt_display
  Verbose: $_verbose"

      if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
        --title "CONFIRM SETTINGS" \
        --ok-button "Create LXC" --cancel-button "Back" \
        --yesno "$summary\n\nCreate ${APP} LXC with these settings?" 32 62; then
        ((STEP++))
      else
        ((STEP--))
      fi
      ;;
    esac
  done

  # ═══════════════════════════════════════════════════════════════════════════
  # Apply all collected values to global variables
  # ═══════════════════════════════════════════════════════════════════════════
  CT_TYPE="$_ct_type"
  PW="$_pw"
  CT_ID="$_ct_id"
  HN="$_hostname"
  DISK_SIZE="$_disk_size"
  CORE_COUNT="$_core_count"
  RAM_SIZE="$_ram_size"
  BRG="$_bridge"
  NET="$_net"
  GATE="$_gate"
  IPV6_METHOD="$_ipv6_method"
  IPV6_ADDR="$_ipv6_addr"
  IPV6_GATE="$_ipv6_gate"
  TAGS="$_tags"
  ENABLE_FUSE="$_enable_fuse"
  ENABLE_TUN="$_enable_tun"
  ENABLE_GPU="$_enable_gpu"
  ENABLE_NESTING="$_enable_nesting"
  ENABLE_KEYCTL="$_enable_keyctl"
  ENABLE_MKNOD="$_enable_mknod"
  ALLOW_MOUNT_FS="$_mount_fs"
  PROTECT_CT="$_protect_ct"
  CT_TIMEZONE="$_ct_timezone"
  APT_CACHER="$_apt_cacher"
  APT_CACHER_IP="$_apt_cacher_ip"
  VERBOSE="$_verbose"

  # Update var_* based on user choice (for functions that check these)
  var_gpu="$_enable_gpu"
  var_fuse="$_enable_fuse"
  var_tun="$_enable_tun"
  var_nesting="$_enable_nesting"
  var_keyctl="$_enable_keyctl"
  var_mknod="$_enable_mknod"
  var_mount_fs="$_mount_fs"
  var_protection="$_protect_ct"
  var_timezone="$_ct_timezone"
  var_apt_cacher="$_apt_cacher"
  var_apt_cacher_ip="$_apt_cacher_ip"

  # Format optional values
  [[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU=""
  [[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD=""
  [[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS=""
  [[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC=""
  [[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN=""

  # Alpine UDHCPC fix
  if [ "$var_os" == "alpine" ] && [ "$NET" == "dhcp" ] && [ -n "$_ns" ]; then
    UDHCPC_FIX="yes"
  else
    UDHCPC_FIX="no"
  fi
  export UDHCPC_FIX
  export SSH_KEYS_FILE

  # Exit alternate screen buffer before showing summary (so output remains visible)
  tput rmcup 2>/dev/null || true
  trap - RETURN

  # Display final summary
  echo -e "\n${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
  echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}"
  echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}"
  echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")${CL}"
  echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
  echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
  echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
  echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
  echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
  echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
  echo -e "${NETWORK}${BOLD}${DGN}IPv4: ${BGN}$NET${CL}"
  echo -e "${NETWORK}${BOLD}${DGN}IPv6: ${BGN}$IPV6_METHOD${CL}"
  echo -e "${FUSE}${BOLD}${DGN}FUSE Support: ${BGN}${ENABLE_FUSE:-no}${CL}"
  [[ "${ENABLE_TUN:-no}" == "yes" ]] && echo -e "${NETWORK}${BOLD}${DGN}TUN/TAP Support: ${BGN}$ENABLE_TUN${CL}"
  echo -e "${CONTAINERTYPE}${BOLD}${DGN}Nesting: ${BGN}$([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")${CL}"
  [[ "${ENABLE_KEYCTL:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Keyctl: ${BGN}Enabled${CL}"
  echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}${ENABLE_GPU:-no}${CL}"
  [[ "${ENABLE_MKNOD:-0}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Mknod: ${BGN}Enabled${CL}"
  [[ -n "${ALLOW_MOUNT_FS:-}" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Mount FS: ${BGN}${ALLOW_MOUNT_FS}${CL}"
  [[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-no}" == "1" ]] && echo -e "${CONTAINERTYPE}${BOLD}${DGN}Protection: ${BGN}Enabled${CL}"
  [[ -n "${CT_TIMEZONE:-}" ]] && echo -e "${INFO}${BOLD}${DGN}Timezone: ${BGN}$CT_TIMEZONE${CL}"
  [[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}"
  echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
  echo -e "${CREATING}${BOLD}${RD}Creating an LXC of ${APP} using the above advanced settings${CL}"

  # Log settings to file
  log_section "CONTAINER SETTINGS (ADVANCED) - ${APP}"
  log_msg "Application: ${APP}"
  log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
  log_msg "Operating System: $var_os ($var_version)"
  log_msg "Container Type: $([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")"
  log_msg "Container ID: $CT_ID"
  log_msg "Hostname: $HN"
  log_msg "Disk Size: ${DISK_SIZE} GB"
  log_msg "CPU Cores: $CORE_COUNT"
  log_msg "RAM Size: ${RAM_SIZE} MiB"
  log_msg "Bridge: $BRG"
  log_msg "IPv4: $NET"
  log_msg "IPv6: $IPV6_METHOD"
  log_msg "FUSE Support: ${ENABLE_FUSE:-no}"
  log_msg "Nesting: $([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")"
  log_msg "Mknod: $([ "${ENABLE_MKNOD:-0}" == "1" ] && echo "Enabled" || echo "Disabled")"
  [[ -n "${ALLOW_MOUNT_FS:-}" ]] && log_msg "Mount FS: ${ALLOW_MOUNT_FS}"
  log_msg "GPU Passthrough: ${ENABLE_GPU:-no}"
  log_msg "Verbose Mode: $VERBOSE"
  log_msg "Session ID: ${SESSION_ID}"
}

# ==============================================================================
# SECTION 7: USER INTERFACE & DIAGNOSTICS
# ==============================================================================

# ------------------------------------------------------------------------------
# diagnostics_check()
#
# - Ensures diagnostics config file exists at /usr/local/community-scripts/diagnostics
# - Asks user whether to send anonymous diagnostic data (first run only)
# - Saves DIAGNOSTICS=yes/no in the config file
# - Reads current diagnostics setting from existing file
# - Sets global DIAGNOSTICS variable for API telemetry opt-in/out
# ------------------------------------------------------------------------------
diagnostics_check() {
  local config_dir="/usr/local/community-scripts"
  local config_file="${config_dir}/diagnostics"

  mkdir -p "$config_dir"

  if [[ -f "$config_file" ]]; then
    DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' "$config_file") || true
    DIAGNOSTICS="${DIAGNOSTICS:-no}"
    return
  fi

  local result
  result=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
    --title "TELEMETRY & DIAGNOSTICS" \
    --ok-button "Confirm" --cancel-button "Exit" \
    --radiolist "\nHelp improve Community-Scripts by sharing anonymous data.\n\nWhat we collect:\n  - Container resources (CPU, RAM, disk), OS & PVE version\n  - Application name, install method and status\n\nWhat we DON'T collect:\n  - No IP addresses, hostnames, or personal data\n\nYou can change this anytime in the Settings menu.\nPrivacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md\n\nUse SPACE to select, ENTER to confirm." 22 76 2 \
    "yes" "Yes, share anonymous data" OFF \
    "no" "No, opt out" OFF \
    3>&1 1>&2 2>&3) || result="no"

  DIAGNOSTICS="${result:-no}"

  cat <<EOF >"$config_file"
DIAGNOSTICS=${DIAGNOSTICS}

# Community-Scripts Telemetry Configuration
# https://telemetry.community-scripts.org
#
# This file stores your telemetry preference.
# Set DIAGNOSTICS=yes to share anonymous installation data.
# Set DIAGNOSTICS=no to disable telemetry.
#
# You can also change this via the Settings menu during installation.
#
# Data collected (when enabled):
#   disk_size, core_count, ram_size, os_type, os_version,
#   nsapp, method, pve_version, status, exit_code
#
# No personal data (IPs, hostnames, passwords) is ever collected.
# Privacy: https://github.com/community-scripts/telemetry-service/blob/main/docs/PRIVACY.md
EOF
}

diagnostics_menu() {
  local current="${DIAGNOSTICS:-no}"
  local status_text="DISABLED"
  [[ "$current" == "yes" ]] && status_text="ENABLED"

  local dialog_text=(
    "Telemetry is currently: ${status_text}\n\n"
    "Anonymous data helps us improve scripts and track issues.\n"
    "No personal data is ever collected.\n\n"
    "More info: https://telemetry.community-scripts.org\n\n"
    "Do you want to ${current:+change this setting}?"
  )

  if [[ "$current" == "yes" ]]; then
    if whiptail --backtitle "Proxmox VE Helper Scripts" \
      --title "TELEMETRY SETTINGS" \
      --yesno "${dialog_text[*]}" 14 64 \
      --yes-button "Disable" --no-button "Keep enabled"; then
      DIAGNOSTICS="no"
      sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics
      whiptail --msgbox "Telemetry disabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
    fi
  else
    if whiptail --backtitle "Proxmox VE Helper Scripts" \
      --title "TELEMETRY SETTINGS" \
      --yesno "${dialog_text[*]}" 14 64 \
      --yes-button "Enable" --no-button "Keep disabled"; then
      DIAGNOSTICS="yes"
      sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics
      whiptail --msgbox "Telemetry enabled.\n\nNote: Existing containers keep their current setting.\nNew containers will inherit this choice." 10 58
    fi
  fi
}

# ------------------------------------------------------------------------------
# echo_default()
#
# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
# - Uses icons and formatting for readability
# - Convert CT_TYPE to description
# - Also logs settings to log file for debugging
# ------------------------------------------------------------------------------
echo_default() {
  CT_TYPE_DESC="Unprivileged"
  if [ "$CT_TYPE" -eq 0 ]; then
    CT_TYPE_DESC="Privileged"
  fi
  echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}"
  echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
  echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
  echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
  echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
  echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
  echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
  if [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]]; then
    echo -e "${GPU}${BOLD}${DGN}GPU Passthrough: ${BGN}Enabled${CL}"
  fi
  if [ "$VERBOSE" == "yes" ]; then
    echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
  fi
  echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
  echo -e "  "

  # Log settings to file
  log_section "CONTAINER SETTINGS - ${APP}"
  log_msg "Application: ${APP}"
  log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
  log_msg "Container ID: ${CT_ID}"
  log_msg "Operating System: $var_os ($var_version)"
  log_msg "Container Type: $CT_TYPE_DESC"
  log_msg "Disk Size: ${DISK_SIZE} GB"
  log_msg "CPU Cores: ${CORE_COUNT}"
  log_msg "RAM Size: ${RAM_SIZE} MiB"
  [[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]] && log_msg "GPU Passthrough: Enabled"
  [[ "$VERBOSE" == "yes" ]] && log_msg "Verbose Mode: Enabled"
  log_msg "Session ID: ${SESSION_ID}"
}

# ------------------------------------------------------------------------------
# install_script()
#
# - Main entrypoint for installation mode
# - Runs safety checks (pve_check, root_check, maxkeys_check, diagnostics_check)
# - Builds interactive menu (Default, Verbose, Advanced, My Defaults, App Defaults, Diagnostics, Storage, Exit)
# - Applies chosen settings and triggers container build
# ------------------------------------------------------------------------------
install_script() {
  pve_check
  shell_check
  root_check
  arch_check
  ssh_check
  maxkeys_check
  diagnostics_check

  if systemctl is-active -q ping-instances.service; then
    systemctl -q stop ping-instances.service
  fi

  NEXTID=$(pvesh get /cluster/nextid)

  # Get timezone using timedatectl (Debian 13+ compatible)
  # Fallback to /etc/timezone for older systems
  if command -v timedatectl >/dev/null 2>&1; then
    timezone=$(timedatectl show --value --property=Timezone 2>/dev/null || echo "UTC")
  elif [ -f /etc/timezone ]; then
    timezone=$(cat /etc/timezone)
  else
    timezone="UTC"
  fi
  [[ "${timezone:-}" == Etc/* ]] && timezone="host" # pct doesn't accept Etc/* zones

  # Show APP Header
  header_info

  # --- Support CLI argument as direct preset (default, advanced, …) ---
  CHOICE="${mode:-${1:-}}"

  # If no CLI argument → show whiptail menu
  # Build menu dynamically based on available options
  local appdefaults_option=""
  local settings_option=""
  local menu_items=(
    "1" "Default Install"
    "2" "Advanced Install"
    "3" "User Defaults"
  )

  if [ -f "$(get_app_defaults_path)" ]; then
    appdefaults_option="4"
    menu_items+=("4" "App Defaults for ${APP}")
    settings_option="5"
    menu_items+=("5" "Settings")
  else
    settings_option="4"
    menu_items+=("4" "Settings")
  fi

  APPDEFAULTS_OPTION="$appdefaults_option"
  SETTINGS_OPTION="$settings_option"

  # Main menu loop - allows returning from Settings
  while true; do
    if [ -z "$CHOICE" ]; then
      TMP_CHOICE=$(whiptail \
        --backtitle "Proxmox VE Helper Scripts" \
        --title "Community-Scripts Options" \
        --ok-button "Select" --cancel-button "Exit Script" \
        --notags \
        --menu "\nChoose an option:\n Use TAB or Arrow keys to navigate, ENTER to select.\n" \
        20 60 9 \
        "${menu_items[@]}" \
        --default-item "1" \
        3>&1 1>&2 2>&3) || exit_script
      CHOICE="$TMP_CHOICE"
    fi

    # --- Main case ---
    local defaults_target=""
    local run_maybe_offer="no"
    case "$CHOICE" in
    1 | default | DEFAULT)
      header_info
      echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
      VERBOSE="no"
      METHOD="default"
      base_settings "$VERBOSE"
      echo_default
      defaults_target="$(ensure_global_default_vars_file)"
      break
      ;;
    2 | advanced | ADVANCED)
      header_info
      echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}"
      METHOD="advanced"
      base_settings
      advanced_settings
      defaults_target="$(ensure_global_default_vars_file)"
      run_maybe_offer="yes"
      break
      ;;
    3 | mydefaults | MYDEFAULTS | userdefaults | USERDEFAULTS)
      default_var_settings || {
        msg_error "Failed to apply default.vars"
        exit 110
      }
      defaults_target="/usr/local/community-scripts/default.vars"
      break
      ;;
    "$APPDEFAULTS_OPTION" | appdefaults | APPDEFAULTS)
      if [ -f "$(get_app_defaults_path)" ]; then
        header_info
        echo -e "${DEFAULT}${BOLD}${BL}Using App Defaults for ${APP} on node $PVEHOST_NAME${CL}"
        METHOD="appdefaults"
        load_vars_file "$(get_app_defaults_path)" "yes" # Force override script defaults
        base_settings
        echo_default
        defaults_target="$(get_app_defaults_path)"
        break
      else
        msg_error "No App Defaults available for ${APP}"
        exit 111
      fi
      ;;
    "$SETTINGS_OPTION" | settings | SETTINGS)
      settings_menu
      # After settings menu, show main menu again
      header_info
      CHOICE=""
      ;;
    generated | GENERATED)
      header_info
      echo -e "${DEFAULT}${BOLD}${BL}Using Generated Settings on node $PVEHOST_NAME${CL}"
      VERBOSE="no"
      METHOD="generated"
      base_settings "$VERBOSE"
      echo_default
      break
      ;;
    *)
      msg_error "Invalid option: $CHOICE"
      exit 112
      ;;
    esac
  done

  if [[ -n "$defaults_target" ]]; then
    ensure_storage_selection_for_vars_file "$defaults_target"
  fi

  if [[ "$run_maybe_offer" == "yes" ]]; then
    maybe_offer_save_app_defaults
  fi
}

edit_default_storage() {
  local vf="/usr/local/community-scripts/default.vars"

  # Ensure file exists
  if [[ ! -f "$vf" ]]; then
    mkdir -p "$(dirname "$vf")"
    touch "$vf"
  fi

  # Let ensure_storage_selection_for_vars_file handle everything
  ensure_storage_selection_for_vars_file "$vf"
}

settings_menu() {
  while true; do
    local settings_items=(
      "1" "Manage API-Diagnostic Setting"
      "2" "Edit Default.vars"
    )
    if [ -f "$(get_app_defaults_path)" ]; then
      settings_items+=("3" "Edit App.vars for ${APP}")
      settings_items+=("4" "Back to Main Menu")
    else
      settings_items+=("3" "Back to Main Menu")
    fi

    local choice
    choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
      --title "Community-Scripts SETTINGS Menu" \
      --ok-button "Select" --cancel-button "Exit Script" \
      --menu "\n\nChoose a settings option:\n\nUse Arrow keys to navigate, ENTER to select, TAB for buttons." 20 60 9 \
      "${settings_items[@]}" \
      3>&1 1>&2 2>&3) || exit_script

    case "$choice" in
    1) diagnostics_menu ;;
    2) ${EDITOR:-nano} /usr/local/community-scripts/default.vars ;;
    3)
      if [ -f "$(get_app_defaults_path)" ]; then
        ${EDITOR:-nano} "$(get_app_defaults_path)"
      else
        # Back was selected (no app.vars available)
        return
      fi
      ;;
    4)
      # Back to main menu
      return
      ;;
    esac
  done
}

# ------------------------------------------------------------------------------
# check_container_resources()
#
# - Compares host RAM/CPU with required values
# - Warns if under-provisioned and asks user to continue or abort
# ------------------------------------------------------------------------------
check_container_resources() {
  current_ram=$(free -m | awk 'NR==2{print $2}')
  current_cpu=$(nproc)

  if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
    msg_warn "Under-provisioned: Required ${var_cpu} CPU/${var_ram}MB RAM, Current ${current_cpu} CPU/${current_ram}MB RAM"
    echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
    echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? <yes/No>  "
    read -r prompt </dev/tty
    if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then
      msg_error "Aborted: under-provisioned LXC (${current_cpu} CPU/${current_ram}MB RAM < ${var_cpu} CPU/${var_ram}MB RAM)"
      exit 113
    fi
  else
    echo -e ""
  fi
}

# ------------------------------------------------------------------------------
# check_container_storage()
#
# - Checks /boot partition usage
# - Warns if usage >80% and asks user confirmation before proceeding
# ------------------------------------------------------------------------------
check_container_storage() {
  total_size=$(df /boot --output=size | tail -n 1)
  local used_size=$(df /boot --output=used | tail -n 1)
  usage=$((100 * used_size / total_size))
  if ((usage > 80)); then
    msg_warn "Storage is dangerously low (${usage}% used on /boot)"
    echo -ne "Continue anyway? <y/N>  "
    read -r prompt </dev/tty
    if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then
      msg_error "Aborted: storage too low (${usage}% used)"
      exit 114
    fi
  fi
}

# ------------------------------------------------------------------------------
# ssh_extract_keys_from_file()
#
# - Extracts valid SSH public keys from given file
# - Supports RSA, Ed25519, ECDSA and filters out comments/invalid lines
# ------------------------------------------------------------------------------
ssh_extract_keys_from_file() {
  local f="$1"
  [[ -r "$f" ]] || return 0
  tr -d '\r' <"$f" | awk '
    /^[[:space:]]*#/ {next}
    /^[[:space:]]*$/ {next}
    # bare format: type base64 [comment]
    /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/ {print; next}
    # with options: find from first key-type onward
    {
      match($0, /(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))[[:space:]]+/)
      if (RSTART>0) { print substr($0, RSTART) }
    }
  '
}

# ------------------------------------------------------------------------------
# ssh_build_choices_from_files()
#
# - Builds interactive whiptail checklist of available SSH keys
# - Generates fingerprint, type and comment for each key
# ------------------------------------------------------------------------------
ssh_build_choices_from_files() {
  local -a files=("$@")
  CHOICES=()
  COUNT=0
  MAPFILE="$(mktemp)"
  local id key typ fp cmt base ln=0

  for f in "${files[@]}"; do
    [[ -f "$f" && -r "$f" ]] || continue
    base="$(basename -- "$f")"
    case "$base" in
    known_hosts | known_hosts.* | config) continue ;;
    id_*) [[ "$f" != *.pub ]] && continue ;;
    esac

    # map every key in file
    while IFS= read -r key; do
      [[ -n "$key" ]] || continue

      typ=""
      fp=""
      cmt=""
      # Only the pure key part (without options) is already included in ‘key’.
      read -r _typ _b64 _cmt <<<"$key"
      typ="${_typ:-key}"
      cmt="${_cmt:-}"
      # Fingerprint via ssh-keygen (if available)
      if command -v ssh-keygen >/dev/null 2>&1; then
        fp="$(printf '%s\n' "$key" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')"
      fi
      # Label shorten
      [[ ${#cmt} -gt 40 ]] && cmt="${cmt:0:37}..."

      ln=$((ln + 1))
      COUNT=$((COUNT + 1))
      id="K${COUNT}"
      echo "${id}|${key}" >>"$MAPFILE"
      CHOICES+=("$id" "[$typ] ${fp:+$fp }${cmt:+$cmt }— ${base}" "OFF")
    done < <(ssh_extract_keys_from_file "$f")
  done
}

# ------------------------------------------------------------------------------
# ssh_discover_default_files()
#
# - Scans standard paths for SSH keys
# - Includes ~/.ssh/*.pub, /etc/ssh/authorized_keys, etc.
# ------------------------------------------------------------------------------
ssh_discover_default_files() {
  local -a cand=()
  shopt -s nullglob
  cand+=(/root/.ssh/authorized_keys /root/.ssh/authorized_keys2)
  cand+=(/root/.ssh/*.pub)
  cand+=(/etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/*)
  shopt -u nullglob
  printf '%s\0' "${cand[@]}"
}

configure_ssh_settings() {
  local step_info="${1:-}"
  local backtitle="Proxmox VE Helper Scripts"
  [[ -n "$step_info" ]] && backtitle="Proxmox VE Helper Scripts [${step_info}]"

  SSH_KEYS_FILE="$(mktemp)"
  : >"$SSH_KEYS_FILE"

  IFS=$'\0' read -r -d '' -a _def_files < <(ssh_discover_default_files && printf '\0')
  ssh_build_choices_from_files "${_def_files[@]}"
  local default_key_count="$COUNT"

  local ssh_key_mode
  if [[ "$default_key_count" -gt 0 ]]; then
    ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
      "Provision SSH keys for root:" 14 72 4 \
      "found" "Select from detected keys (${default_key_count})" \
      "manual" "Paste a single public key" \
      "folder" "Scan another folder (path or glob)" \
      "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
  else
    ssh_key_mode=$(whiptail --backtitle "$backtitle" --title "SSH KEY SOURCE" --menu \
      "No host keys detected; choose manual/none:" 12 72 2 \
      "manual" "Paste a single public key" \
      "none" "No keys" 3>&1 1>&2 2>&3) || exit_script
  fi

  case "$ssh_key_mode" in
  found)
    local selection
    selection=$(whiptail --backtitle "$backtitle" --title "SELECT HOST KEYS" \
      --checklist "Select one or more keys to import:" 20 140 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
    for tag in $selection; do
      tag="${tag%\"}"
      tag="${tag#\"}"
      local line
      line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
      [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
    done
    ;;
  manual)
    SSH_AUTHORIZED_KEY="$(whiptail --backtitle "$backtitle" \
      --inputbox "Paste one SSH public key line (ssh-ed25519/ssh-rsa/...)" 10 72 --title "SSH Public Key" 3>&1 1>&2 2>&3)"
    [[ -n "$SSH_AUTHORIZED_KEY" ]] && printf '%s\n' "$SSH_AUTHORIZED_KEY" >>"$SSH_KEYS_FILE"
    ;;
  folder)
    local glob_path
    glob_path=$(whiptail --backtitle "$backtitle" \
      --inputbox "Enter a folder or glob to scan (e.g. /root/.ssh/*.pub)" 10 72 --title "Scan Folder/Glob" 3>&1 1>&2 2>&3)
    if [[ -n "$glob_path" ]]; then
      shopt -s nullglob
      read -r -a _scan_files <<<"$glob_path"
      shopt -u nullglob
      if [[ "${#_scan_files[@]}" -gt 0 ]]; then
        ssh_build_choices_from_files "${_scan_files[@]}"
        if [[ "$COUNT" -gt 0 ]]; then
          local folder_selection
          folder_selection=$(whiptail --backtitle "$backtitle" --title "SELECT FOLDER KEYS" \
            --checklist "Select key(s) to import:" 20 78 10 "${CHOICES[@]}" 3>&1 1>&2 2>&3) || exit_script
          for tag in $folder_selection; do
            tag="${tag%\"}"
            tag="${tag#\"}"
            local line
            line=$(grep -E "^${tag}\|" "$MAPFILE" | head -n1 | cut -d'|' -f2- || true)
            [[ -n "$line" ]] && printf '%s\n' "$line" >>"$SSH_KEYS_FILE"
          done
        else
          whiptail --backtitle "$backtitle" --msgbox "No keys found in: $glob_path" 8 60
        fi
      else
        whiptail --backtitle "$backtitle" --msgbox "Path/glob returned no files." 8 60
      fi
    fi
    ;;
  none)
    :
    ;;
  esac

  if [[ -s "$SSH_KEYS_FILE" ]]; then
    sort -u -o "$SSH_KEYS_FILE" "$SSH_KEYS_FILE"
    printf '\n' >>"$SSH_KEYS_FILE"
  fi

  # Always show SSH access dialog - user should be able to enable SSH even without keys
  if (whiptail --backtitle "$backtitle" --defaultno --title "SSH ACCESS" --yesno "Enable root SSH access?" 10 58); then
    SSH="yes"
  else
    SSH="no"
  fi
}

# ------------------------------------------------------------------------------
# msg_menu()
#
# - Displays a numbered menu for update_script() functions
# - In silent mode (PHS_SILENT=1): auto-selects the default option
# - In interactive mode: shows menu via read with 10s timeout + default fallback
# - Usage: CHOICE=$(msg_menu "Title" "tag1" "Description 1" "tag2" "Desc 2" ...)
# - The first item is always the default
# - Returns the selected tag to stdout
# - If no valid selection or timeout, returns the default (first) tag
# ------------------------------------------------------------------------------
msg_menu() {
  local title="$1"
  shift

  # Parse items into parallel arrays: tags[] and descriptions[]
  local -a tags=()
  local -a descs=()
  while [[ $# -ge 2 ]]; do
    tags+=("$1")
    descs+=("$2")
    shift 2
  done

  local default_tag="${tags[0]}"
  local count=${#tags[@]}

  # Silent mode: return default immediately
  if [[ -n "${PHS_SILENT+x}" ]] && [[ "${PHS_SILENT}" == "1" ]]; then
    echo "$default_tag"
    return 0
  fi

  # Display menu to /dev/tty so it doesn't get captured by command substitution
  {
    echo ""
    msg_custom "📋" "${BL}" "${title}"
    echo ""
    for i in "${!tags[@]}"; do
      local marker="  "
      [[ $i -eq 0 ]] && marker="* "
      printf "${TAB3}${marker}%s) %s\n" "${tags[$i]}" "${descs[$i]}"
    done
    echo ""
  } >/dev/tty

  local selection=""
  read -r -t 10 -p "${TAB3}Select [default=${default_tag}, timeout 10s]: " selection </dev/tty >/dev/tty || true

  # Validate selection
  if [[ -n "$selection" ]]; then
    for tag in "${tags[@]}"; do
      if [[ "$selection" == "$tag" ]]; then
        echo "$selection"
        return 0
      fi
    done
    msg_warn "Invalid selection '${selection}' - using default: ${default_tag}"
  fi

  echo "$default_tag"
  return 0
}

# ------------------------------------------------------------------------------
# start()
#
# - Entry point of script
# - On Proxmox host: calls install_script
# - In silent mode: runs update_script with automatic cleanup
# - Otherwise: shows update/setting menu and runs update_script with cleanup
# ------------------------------------------------------------------------------
start() {
  source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
  if command -v pveversion >/dev/null 2>&1; then
    install_script || return 0
    return 0
  elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then
    VERBOSE="no"
    set_std_mode
    ensure_profile_loaded
    get_lxc_ip
    update_script
    update_motd_ip
    cleanup_lxc
  else
    CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
      "Support/Update functions for ${APP} LXC. Choose an option:" \
      12 60 3 \
      "1" "YES (Silent Mode)" \
      "2" "YES (Verbose Mode)" \
      "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)

    case "$CHOICE" in
    1)
      VERBOSE="no"
      set_std_mode
      ;;
    2)
      VERBOSE="yes"
      set_std_mode
      ;;
    3)
      clear
      exit_script
      exit 0
      ;;
    esac
    ensure_profile_loaded
    get_lxc_ip
    update_script
    update_motd_ip
    cleanup_lxc
  fi
}

# ==============================================================================
# SECTION 8: CONTAINER CREATION & DEPLOYMENT
# ==============================================================================

# ------------------------------------------------------------------------------
# build_container()
#
# - Main function for creating and configuring LXC container
# - Builds network configuration string (IP, gateway, VLAN, MTU, MAC, IPv6)
# - Creates container via pct create with all specified settings
# - Applies features: FUSE, TUN, keyctl, VAAPI passthrough
# - Starts container and waits for network connectivity
# - Installs base packages (curl, sudo, etc.)
# - Injects SSH keys if configured
# - Executes <app>-install.sh inside container
# - Posts installation telemetry to API if diagnostics enabled
# ------------------------------------------------------------------------------
build_container() {
  #  if [ "$VERBOSE" == "yes" ]; then set -x; fi

  NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"

  # MAC
  if [[ -n "$MAC" ]]; then
    case "$MAC" in
    ,hwaddr=*) NET_STRING+="$MAC" ;;
    *) NET_STRING+=",hwaddr=$MAC" ;;
    esac
  fi

  # IP (always required, default dhcp)
  NET_STRING+=",ip=${NET:-dhcp}"

  # Gateway
  if [[ -n "$GATE" ]]; then
    case "$GATE" in
    ,gw=) ;;
    ,gw=*) NET_STRING+="$GATE" ;;
    *) NET_STRING+=",gw=$GATE" ;;
    esac
  fi

  # VLAN
  if [[ -n "$VLAN" ]]; then
    case "$VLAN" in
    ,tag=*) NET_STRING+="$VLAN" ;;
    *) NET_STRING+=",tag=$VLAN" ;;
    esac
  fi

  # MTU
  if [[ -n "$MTU" ]]; then
    case "$MTU" in
    ,mtu=*) NET_STRING+="$MTU" ;;
    *) NET_STRING+=",mtu=$MTU" ;;
    esac
  fi

  # IPv6 Handling
  case "$IPV6_METHOD" in
  auto) NET_STRING="$NET_STRING,ip6=auto" ;;
  dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
  static)
    if [[ -n "$IPV6_ADDR" ]]; then
      NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
      [ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
    fi
    ;;
  none) ;;
  esac

  # Build FEATURES string based on container type and user choices
  FEATURES=""

  # Nesting support (user configurable, default enabled)
  if [ "${ENABLE_NESTING:-1}" == "1" ]; then
    FEATURES="nesting=1"
  fi

  # Keyctl for unprivileged containers (needed for Docker)
  if [ "$CT_TYPE" == "1" ]; then
    [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
    FEATURES="${FEATURES}keyctl=1"
  fi

  if [ "$ENABLE_FUSE" == "yes" ]; then
    [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
    FEATURES="${FEATURES}fuse=1"
  fi

  # Mknod support (user configurable via advanced settings)
  if [ "${ENABLE_MKNOD:-0}" == "1" ]; then
    [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
    FEATURES="${FEATURES}mknod=1"
  fi

  # Mount filesystem types (user configurable via advanced settings)
  if [ -n "${ALLOW_MOUNT_FS:-}" ]; then
    [ -n "$FEATURES" ] && FEATURES="$FEATURES,"
    FEATURES="${FEATURES}mount=${ALLOW_MOUNT_FS//,/;}"
  fi

  # Build PCT_OPTIONS as string for export
  local _func_url
  if [ "$var_os" == "alpine" ]; then
    _func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func"
  else
    _func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func"
  fi
  export FUNCTIONS_FILE_PATH="$(curl -fsSL "$_func_url")"
  if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then
    msg_error "Failed to download install functions from: $_func_url"
    exit 115
  fi

  # Core exports for install.func
  export DIAGNOSTICS="$DIAGNOSTICS"
  export RANDOM_UUID="$RANDOM_UUID"
  export EXECUTION_ID="$EXECUTION_ID"
  export SESSION_ID="$SESSION_ID"
  export CACHER="$APT_CACHER"
  export CACHER_IP="$APT_CACHER_IP"
  export tz="$timezone"
  export APPLICATION="$APP"
  export app="$NSAPP"
  export PASSWORD="$PW"
  export VERBOSE="$VERBOSE"
  export SSH_ROOT="${SSH}"
  export SSH_AUTHORIZED_KEY
  export CTID="$CT_ID"
  export CTTYPE="$CT_TYPE"
  export ENABLE_FUSE="$ENABLE_FUSE"
  export ENABLE_TUN="$ENABLE_TUN"
  export PCT_OSTYPE="$var_os"
  export PCT_OSVERSION="$var_version"
  export PCT_DISK_SIZE="$DISK_SIZE"
  export IPV6_METHOD="$IPV6_METHOD"
  export ENABLE_GPU="$ENABLE_GPU"

  # DEV_MODE exports (optional, for debugging)
  export BUILD_LOG="$BUILD_LOG"
  export INSTALL_LOG="/root/.install-${SESSION_ID}.log"

  # Keep host-side logging on BUILD_LOG (not exported — invisible to container)
  # Without this, get_active_logfile() would return INSTALL_LOG (a container path)
  # and all host msg_info/msg_ok/msg_error would write to /root/.install-SESSION.log
  # on the HOST instead of BUILD_LOG, causing incomplete telemetry logs.
  _HOST_LOGFILE="$BUILD_LOG"

  export dev_mode="${dev_mode:-}"
  export DEV_MODE_MOTD="${DEV_MODE_MOTD:-false}"
  export DEV_MODE_KEEP="${DEV_MODE_KEEP:-false}"
  export DEV_MODE_TRACE="${DEV_MODE_TRACE:-false}"
  export DEV_MODE_PAUSE="${DEV_MODE_PAUSE:-false}"
  export DEV_MODE_BREAKPOINT="${DEV_MODE_BREAKPOINT:-false}"
  export DEV_MODE_LOGS="${DEV_MODE_LOGS:-false}"
  export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}"

  # Build PCT_OPTIONS as multi-line string
  PCT_OPTIONS_STRING="  -hostname $HN"

  # Only add -tags if TAGS is not empty
  if [ -n "$TAGS" ]; then
    PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
  -tags $TAGS"
  fi

  # Only add -features if FEATURES is not empty
  if [ -n "$FEATURES" ]; then
    PCT_OPTIONS_STRING="  -features $FEATURES
$PCT_OPTIONS_STRING"
  fi

  # Add searchdomain if specified
  if [ -n "$SD" ]; then
    PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
  $SD"
  fi

  # Add nameserver if specified
  if [ -n "$NS" ]; then
    PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
  $NS"
  fi

  # Network configuration
  PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
  $NET_STRING
  -onboot 1
  -cores $CORE_COUNT
  -memory $RAM_SIZE
  -unprivileged $CT_TYPE"

  # Protection flag (if var_protection was set)
  if [ "${PROTECT_CT:-}" == "1" ] || [ "${PROTECT_CT:-}" == "yes" ]; then
    PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
  -protection 1"
  fi

  # Timezone (map Etc/* to "host" as pct doesn't accept them)
  if [ -n "${CT_TIMEZONE:-}" ]; then
    local _pct_timezone="$CT_TIMEZONE"
    [[ "$_pct_timezone" == Etc/* ]] && _pct_timezone="host"
    PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
  -timezone $_pct_timezone"
  fi

  # Password (already formatted)
  if [ -n "$PW" ]; then
    PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING
  $PW"
  fi

  # Export as string (this works, unlike arrays!)
  export PCT_OPTIONS="$PCT_OPTIONS_STRING"
  export TEMPLATE_STORAGE="${var_template_storage:-}"
  export CONTAINER_STORAGE="${var_container_storage:-}"

  # Validate storage space only if CONTAINER_STORAGE is already set
  # (Storage selection happens in create_lxc_container for some modes)
  if [[ -n "$CONTAINER_STORAGE" ]]; then
    msg_info "Validating storage space"
    if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then
      local free_space
      free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
      local free_fmt
      free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB")
      msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}"
      exit 214
    fi
    msg_ok "Storage space validated"
  fi

  create_lxc_container || exit $?

  # Transition to 'configuring' — container created, now setting up OS/userland
  post_progress_to_api "configuring"

  LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"

  # ============================================================================
  # GPU/USB PASSTHROUGH CONFIGURATION
  # ============================================================================

  # Check if GPU passthrough is enabled
  # Returns true only if var_gpu is explicitly set to "yes"
  # Can be set via:
  #   - Environment variable: var_gpu=yes bash -c "..."
  #   - CT script default: var_gpu="${var_gpu:-no}"
  #   - Advanced settings wizard
  #   - App defaults file: /usr/local/community-scripts/defaults/<app>.vars
  is_gpu_app() {
    [[ "${var_gpu:-no}" == "yes" ]] && return 0
    return 1
  }

  # Detect all available GPU devices
  detect_gpu_devices() {
    INTEL_DEVICES=()
    AMD_DEVICES=()
    NVIDIA_DEVICES=()

    # Store PCI info to avoid multiple calls
    # grep returns exit 1 when no match — use || true to prevent ERR trap
    local pci_vga_info
    pci_vga_info=$(lspci -nn 2>/dev/null | grep -E "VGA|Display|3D" || true)

    # No GPU-related PCI devices at all? Skip silently.
    if [[ -z "$pci_vga_info" ]]; then
      msg_debug "No VGA/Display/3D PCI devices found"
      return 0
    fi

    # Check for Intel GPU - look for Intel vendor ID [8086]
    if grep -q "\[8086:" <<<"$pci_vga_info"; then
      msg_custom "🎮" "${BL}" "Detected Intel GPU"
      if [[ -d /dev/dri ]]; then
        for d in /dev/dri/renderD* /dev/dri/card*; do
          [[ -e "$d" ]] && INTEL_DEVICES+=("$d")
        done
      fi
    fi

    # Check for AMD GPU - look for AMD vendor IDs [1002] (AMD/ATI) or [1022] (AMD)
    if grep -qE "\[1002:|\[1022:" <<<"$pci_vga_info"; then
      msg_custom "🎮" "${RD}" "Detected AMD GPU"
      if [[ -d /dev/dri ]]; then
        # Only add if not already claimed by Intel
        if [[ ${#INTEL_DEVICES[@]} -eq 0 ]]; then
          for d in /dev/dri/renderD* /dev/dri/card*; do
            [[ -e "$d" ]] && AMD_DEVICES+=("$d")
          done
        fi
      fi
    fi

    # Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
    if grep -q "\[10de:" <<<"$pci_vga_info"; then
      msg_custom "🎮" "${GN}" "Detected NVIDIA GPU"

      # Simple passthrough - just bind /dev/nvidia* devices if they exist
      # Only include character devices (-c), skip directories like /dev/nvidia-caps
      for d in /dev/nvidia*; do
        [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
      done
      # Also check for devices inside /dev/nvidia-caps/ directory
      if [[ -d /dev/nvidia-caps ]]; then
        for d in /dev/nvidia-caps/*; do
          [[ -c "$d" ]] && NVIDIA_DEVICES+=("$d")
        done
      fi

      if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
        msg_custom "🎮" "${GN}" "Found ${#NVIDIA_DEVICES[@]} NVIDIA device(s) for passthrough"
      else
        msg_warn "NVIDIA GPU detected via PCI but no /dev/nvidia* devices found"
        msg_custom "ℹ️" "${YW}" "Skipping NVIDIA passthrough (host drivers may not be loaded)"
      fi
    fi

    # Debug output
    msg_debug "Intel devices: ${INTEL_DEVICES[*]}"
    msg_debug "AMD devices: ${AMD_DEVICES[*]}"
    msg_debug "NVIDIA devices: ${NVIDIA_DEVICES[*]}"
  }

  # Configure USB passthrough for privileged containers
  configure_usb_passthrough() {
    if [[ "$CT_TYPE" != "0" ]]; then
      return 0
    fi

    msg_info "Configuring automatic USB passthrough (privileged container)"
    cat <<EOF >>"$LXC_CONFIG"
# Automatic USB passthrough (privileged container)
lxc.cgroup2.devices.allow: a
lxc.cap.drop:
lxc.cgroup2.devices.allow: c 188:* rwm
lxc.cgroup2.devices.allow: c 189:* rwm
lxc.mount.entry: /dev/serial/by-id  dev/serial/by-id  none bind,optional,create=dir
lxc.mount.entry: /dev/ttyUSB0       dev/ttyUSB0       none bind,optional,create=file
lxc.mount.entry: /dev/ttyUSB1       dev/ttyUSB1       none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM0       dev/ttyACM0       none bind,optional,create=file
lxc.mount.entry: /dev/ttyACM1       dev/ttyACM1       none bind,optional,create=file
EOF
    msg_ok "USB passthrough configured"
  }

  # Configure GPU passthrough
  configure_gpu_passthrough() {
    # Skip if:
    # GPU passthrough is enabled when var_gpu="yes":
    # - Set via environment variable: var_gpu=yes bash -c "..."
    # - Set in CT script: var_gpu="${var_gpu:-no}"
    # - Enabled in advanced_settings wizard
    # - Configured in app defaults file
    if ! is_gpu_app "$APP"; then
      return 0
    fi

    detect_gpu_devices

    # Count available GPU types
    local gpu_count=0
    local available_gpus=()

    if [[ ${#INTEL_DEVICES[@]} -gt 0 ]]; then
      available_gpus+=("INTEL")
      gpu_count=$((gpu_count + 1))
    fi

    if [[ ${#AMD_DEVICES[@]} -gt 0 ]]; then
      available_gpus+=("AMD")
      gpu_count=$((gpu_count + 1))
    fi

    if [[ ${#NVIDIA_DEVICES[@]} -gt 0 ]]; then
      available_gpus+=("NVIDIA")
      gpu_count=$((gpu_count + 1))
    fi

    if [[ $gpu_count -eq 0 ]]; then
      msg_custom "ℹ️" "${YW}" "No GPU devices found for passthrough"
      return 0
    fi

    local selected_gpu=""

    if [[ $gpu_count -eq 1 ]]; then
      # Automatic selection for single GPU
      selected_gpu="${available_gpus[0]}"
      msg_ok "Automatically configuring ${selected_gpu} GPU passthrough"
    else
      # Multiple GPUs - ask user
      echo -e "\n${INFO} Multiple GPU types detected:"
      for gpu in "${available_gpus[@]}"; do
        echo "  - $gpu"
      done
      read -rp "Which GPU type to passthrough? (${available_gpus[*]}): " selected_gpu </dev/tty
      selected_gpu="${selected_gpu^^}"

      # Validate selection
      local valid=0
      for gpu in "${available_gpus[@]}"; do
        [[ "$selected_gpu" == "$gpu" ]] && valid=1
      done

      if [[ $valid -eq 0 ]]; then
        msg_warn "Invalid selection. Skipping GPU passthrough."
        return 0
      fi
    fi

    # Apply passthrough configuration based on selection
    local dev_idx=0

    case "$selected_gpu" in
    INTEL | AMD)
      local devices=()
      [[ "$selected_gpu" == "INTEL" ]] && devices=("${INTEL_DEVICES[@]}")
      [[ "$selected_gpu" == "AMD" ]] && devices=("${AMD_DEVICES[@]}")

      # Use pct set to add devices with proper dev0/dev1 format
      # GIDs will be detected and set after container starts
      local dev_index=0
      for dev in "${devices[@]}"; do
        # Add to config using pct set (will be visible in GUI)
        echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
        dev_index=$((dev_index + 1))
      done

      export GPU_TYPE="$selected_gpu"
      msg_ok "${selected_gpu} GPU passthrough configured (${#devices[@]} devices)"
      ;;

    NVIDIA)
      if [[ ${#NVIDIA_DEVICES[@]} -eq 0 ]]; then
        msg_warn "No NVIDIA devices available for passthrough"
        return 0
      fi

      # Use pct set for NVIDIA devices
      local dev_index=0
      for dev in "${NVIDIA_DEVICES[@]}"; do
        echo "dev${dev_index}: ${dev},gid=44" >>"$LXC_CONFIG"
        dev_index=$((dev_index + 1))
      done

      export GPU_TYPE="NVIDIA"
      msg_ok "NVIDIA GPU passthrough configured (${#NVIDIA_DEVICES[@]} devices) - install drivers in container if needed"
      ;;
    esac
  }

  # Additional device passthrough
  configure_additional_devices() {
    # TUN device passthrough
    if [ "$ENABLE_TUN" == "yes" ]; then
      cat <<EOF >>"$LXC_CONFIG"
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
EOF
    fi

    # Coral TPU passthrough
    if [[ -e /dev/apex_0 ]]; then
      msg_custom "🔌" "${BL}" "Detected Coral TPU - configuring passthrough"
      echo "lxc.mount.entry: /dev/apex_0 dev/apex_0 none bind,optional,create=file" >>"$LXC_CONFIG"
    fi
  }

  # Execute pre-start configurations
  configure_usb_passthrough
  configure_gpu_passthrough
  configure_additional_devices

  # Increase disk size for AMD ROCm runtime (~4GB extra needed)
  if [[ "${GPU_TYPE:-}" == "AMD" ]]; then
    local rocm_extra=4
    local new_disk_size=$((PCT_DISK_SIZE + rocm_extra))
    if pct resize "$CTID" rootfs "${new_disk_size}G" >/dev/null 2>&1; then
      msg_ok "Disk resized ${PCT_DISK_SIZE}GB → ${new_disk_size}GB for ROCm"
    else
      msg_warn "Failed to resize disk for ROCm — installation may fail if space is insufficient"
    fi
  fi

  # ============================================================================
  # START CONTAINER AND INSTALL USERLAND
  # ============================================================================

  msg_info "Starting LXC Container"
  pct start "$CTID"

  # Wait for container to be running
  for i in {1..10}; do
    if pct status "$CTID" | grep -q "status: running"; then
      msg_ok "Started LXC Container"
      break
    fi
    sleep 1
    if [ "$i" -eq 10 ]; then
      local ct_status
      ct_status=$(pct status "$CTID" 2>/dev/null || echo "unknown")
      msg_error "LXC Container did not reach running state (status: ${ct_status})"
      exit 117
    fi
  done

  # Wait for network (skip for Alpine initially)
  if [ "$var_os" != "alpine" ]; then
    msg_info "Waiting for network in LXC container"

    # Wait for IP assignment (IPv4 or IPv6)
    local ip_in_lxc=""
    for i in {1..60}; do
      # Try IPv4 first
      ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
      # Fallback to IPv6 if IPv4 not available
      if [ -z "$ip_in_lxc" ]; then
        ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
      fi
      [ -n "$ip_in_lxc" ] && break
      # Progressive backoff: 1s for first 20, 2s for next 20, 3s for last 20
      if [ "$i" -le 20 ]; then
        sleep 1
      elif [ "$i" -le 40 ]; then
        sleep 2
      else
        sleep 3
      fi
    done

    if [ -z "$ip_in_lxc" ]; then
      msg_error "No IP assigned to CT $CTID after 60 attempts"
      msg_custom "🔧" "${YW}" "Troubleshooting:"
      echo "  • Verify bridge ${BRG} exists and has connectivity"
      echo "  • Check if DHCP server is reachable (if using DHCP)"
      echo "  • Verify static IP configuration (if using static IP)"
      echo "  • Check Proxmox firewall rules"
      echo "  • If using Tailscale: Disable MagicDNS temporarily"
      exit 118
    fi

    # Verify basic connectivity (ping test)
    local ping_success=false
    for retry in {1..3}; do
      if pct exec "$CTID" -- ping -c 1 -W 2 1.1.1.1 &>/dev/null ||
        pct exec "$CTID" -- ping -c 1 -W 2 8.8.8.8 &>/dev/null ||
        pct exec "$CTID" -- ping6 -c 1 -W 2 2606:4700:4700::1111 &>/dev/null; then
        ping_success=true
        break
      fi
      sleep 2
    done

    if [ "$ping_success" = false ]; then
      msg_warn "Network configured (IP: $ip_in_lxc) but connectivity test failed - installation will continue"
    else
      msg_ok "Network in LXC is reachable (ping)"
    fi
  fi
  # Function to get correct GID inside container
  get_container_gid() {
    local group="$1"
    local gid=$(pct exec "$CTID" -- getent group "$group" 2>/dev/null | cut -d: -f3)
    echo "${gid:-44}" # Default to 44 if not found
  }

  fix_gpu_gids

  # Fix Debian 13 LXC template bug where / is owned by nobody:nogroup
  # This must be done from the host as unprivileged containers cannot chown /
  local rootfs
  rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1 || true)
  if [[ -n "$rootfs" ]]; then
    local mount_point="/var/lib/lxc/${CTID}/rootfs"
    if [[ -d "$mount_point" ]] && [[ "$(stat -c '%U' "$mount_point")" != "root" ]]; then
      chown root:root "$mount_point" 2>/dev/null || true
    fi
  fi

  # Continue with standard container setup
  msg_info "Customizing LXC Container"

  # # Install GPU userland if configured
  # if [[ "${ENABLE_VAAPI:-0}" == "1" ]]; then
  #   install_gpu_userland "VAAPI"
  # fi

  # if [[ "${ENABLE_NVIDIA:-0}" == "1" ]]; then
  #   install_gpu_userland "NVIDIA"
  # fi

  # Disable error trap for entire customization & install phase.
  # All errors are handled explicitly — recovery menu shown on failure.
  # Without this, customization errors (e.g. container stopped during base package
  # install) would trigger error_handler() with a simple "Remove broken container?"
  # prompt instead of the full recovery menu with retry/repair options.
  set +Eeuo pipefail
  trap - ERR

  local install_exit_code=0

  # Continue with standard container setup
  if [ "$var_os" == "alpine" ]; then
    sleep 3
    pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
EOF'
    pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq" >>"$BUILD_LOG" 2>&1 || {
      msg_warn "apk install failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
      local alpine_exit=0
      pct exec "$CTID" -- ash -c '
        ALPINE_MIRRORS="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
        for m in $(printf "%s\n" $ALPINE_MIRRORS | shuf); do
          if wget -q --spider --timeout=2 "http://$m/alpine/latest-stable/main/" 2>/dev/null; then
            echo "  Attempting mirror: $m"
            cat <<EOF >/etc/apk/repositories
http://$m/alpine/latest-stable/main
http://$m/alpine/latest-stable/community
EOF
            if apk update >/dev/null 2>&1 && apk add bash newt curl openssh nano mc ncurses jq >/dev/null 2>&1; then
              echo "  CDN set to $m: tests passed"
              exit 0
            else
              echo "  Mirror $m failed"
            fi
          fi
        done
        exit 2
      ' && alpine_exit=0 || alpine_exit=$?
      if [[ $alpine_exit -ne 0 ]]; then
        msg_error "Failed to install base packages in Alpine container"
        install_exit_code=1
      fi
    }
  else
    sleep 3
    LANG=${LANG:-en_US.UTF-8}
    local LANG_ESC="${LANG//./\\.}"
    LANG_ESC="${LANG_ESC//|/\\|}"
    pct exec "$CTID" -- bash -c "sed -i \"/$LANG_ESC/ s/^# //\" /etc/locale.gen"
    pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
    echo LANG=\$locale_line >/etc/default/locale && \
    locale-gen >/dev/null && \
    export LANG=\$locale_line"

    if [[ -z "${tz:-}" ]]; then
      tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC")
    fi
    [[ "${tz:-}" == Etc/* ]] && tz="UTC" # Normalize Etc/* to UTC for container setup

    if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
      # Set timezone using symlink (Debian 13+ compatible)
      # Create /etc/timezone for backwards compatibility with older scripts
      pct exec "$CTID" -- bash -c "tz='$tz'; ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime && echo \"\$tz\" >/etc/timezone || true"
    else
      msg_warn "Skipping timezone setup – zone '$tz' not found in container"
    fi

    # Detect broken DNS resolver (e.g. Tailscale MagicDNS) and inject public DNS
    if ! pct exec "$CTID" -- bash -c "getent hosts deb.debian.org >/dev/null 2>&1 && getent hosts archive.ubuntu.com >/dev/null 2>&1"; then
      msg_warn "APT repository DNS resolution failed in container, injecting public DNS servers"
      pct exec "$CTID" -- bash -c "echo -e 'nameserver 8.8.8.8\nnameserver 1.1.1.1' >/etc/resolv.conf"
    fi

    pct exec "$CTID" -- bash -c "apt-get update 2>&1 && apt-get install -y sudo curl mc gnupg2 jq 2>&1" >>"$BUILD_LOG" 2>&1 || {
      local failed_mirror
      failed_mirror=$(pct exec "$CTID" -- bash -c "grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null" 2>/dev/null || echo "unknown")
      msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..."
      local mirror_exit=0
      pct exec "$CTID" -- bash -c '
        APT_BASE="sudo curl mc gnupg2 jq"
        DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")

        if [ "$DISTRO" = "ubuntu" ]; then
          EU_MIRRORS="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de"
          US_MIRRORS="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu"
          AP_MIRRORS="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au"
        else
          EU_MIRRORS="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
          US_MIRRORS="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
          AP_MIRRORS="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
        fi

        TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC")
        case "$TZ" in
          Europe/*|Arctic/*) REGIONAL="$EU_MIRRORS"; OTHERS="$US_MIRRORS $AP_MIRRORS" ;;
          America/*) REGIONAL="$US_MIRRORS"; OTHERS="$EU_MIRRORS $AP_MIRRORS" ;;
          Asia/*|Australia/*|Pacific/*) REGIONAL="$AP_MIRRORS"; OTHERS="$EU_MIRRORS $US_MIRRORS" ;;
          *) REGIONAL=""; OTHERS="$EU_MIRRORS $US_MIRRORS $AP_MIRRORS" ;;
        esac

        echo "Acquire::By-Hash \"no\";" >/etc/apt/apt.conf.d/99no-by-hash

        try_mirrors() {
          for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
            [ -f "$src" ] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: http://${1}/|g; s|deb http[s]*://[^/]*/|deb http://${1}/|g" "$src"
          done
          rm -rf /var/lib/apt/lists/*
          APT_OUT=$(apt-get update 2>&1)
          APT_RC=$?
          if echo "$APT_OUT" | grep -qi "hashsum\|hash sum"; then
            echo "  Mirror $1 failed (hash mismatch)"
            return 1
          elif echo "$APT_OUT" | grep -qi "SSL\|certificate"; then
            echo "  Mirror $1 failed (SSL/certificate error)"
            return 1
          elif [ $APT_RC -ne 0 ]; then
            echo "  Mirror $1 failed (apt-get update error)"
            return 1
          elif apt-get install -y $APT_BASE >/dev/null 2>&1; then
            echo "  CDN set to $1: tests passed"
            return 0
          else
            echo "  Mirror $1 failed (package install error)"
            return 1
          fi
        }

        scan_reachable() {
          local result=""
          for m in $1; do
            if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
              result="$result $m"
            fi
          done
          echo "$result" | xargs
        }

        # Phase 1: Scan global mirrors first (independent of local CDN issues)
        OTHERS_OK=$(scan_reachable "$OTHERS")
        OTHERS_PICK=$(printf "%s\n" $OTHERS_OK | shuf | head -3 | xargs)

        for mirror in $OTHERS_PICK; do
          echo "  Attempting mirror: $mirror"
          try_mirrors "$mirror" && exit 0
        done

        # Phase 2: Try primary mirror
        if [ "$DISTRO" = "ubuntu" ]; then
          PRIMARY="archive.ubuntu.com"
        else
          PRIMARY="ftp.debian.org"
        fi
        if timeout 2 bash -c "echo >/dev/tcp/$PRIMARY/80" 2>/dev/null; then
          echo "  Attempting mirror: $PRIMARY"
          try_mirrors "$PRIMARY" && exit 0
        fi

        # Phase 3: Fall back to regional mirrors
        REGIONAL_OK=$(scan_reachable "$REGIONAL")
        REGIONAL_PICK=$(printf "%s\n" $REGIONAL_OK | shuf | head -3 | xargs)

        for mirror in $REGIONAL_PICK; do
          echo "  Attempting mirror: $mirror"
          try_mirrors "$mirror" && exit 0
        done

        exit 2
      ' && mirror_exit=0 || mirror_exit=$?
      if [[ $mirror_exit -eq 2 ]]; then
        msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
        if [[ "$var_os" == "ubuntu" ]]; then
          msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors"
        else
          msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list"
        fi
        local custom_mirror=""
        while true; do
          read -rp "  Enter a mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
          [[ -z "$custom_mirror" ]] && continue
          [[ "$custom_mirror" == "skip" ]] && break
          [[ ! "$custom_mirror" =~ ^[a-zA-Z0-9._-]+$ ]] && {
            msg_warn "Invalid hostname format."
            continue
          }
          pct exec "$CTID" -- bash -c "
            for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
              [ -f \"\$src\" ] && sed -i \"s|URIs: http[s]*://[^/]*/|URIs: http://${custom_mirror}/|g; s|deb http[s]*://[^/]*/|deb http://${custom_mirror}/|g\" \"\$src\"
            done
            rm -rf /var/lib/apt/lists/*
            apt-get update >/dev/null 2>&1 && apt-get install -y sudo curl mc gnupg2 jq >/dev/null 2>&1
          " && break
          msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
        done
        if [[ "$custom_mirror" == "skip" ]]; then
          msg_error "apt-get base packages installation failed"
          install_exit_code=1
        fi
      elif [[ $mirror_exit -ne 0 ]]; then
        msg_error "apt-get base packages installation failed"
        install_exit_code=1
      fi
    }
  fi

  # Only continue with installation if customization succeeded
  if [[ $install_exit_code -eq 0 ]]; then
    msg_ok "Customized LXC Container"

    # Optional DNS override for retry scenarios (inside LXC, never on host)
    if [[ "${DNS_RETRY_OVERRIDE:-false}" == "true" ]]; then
      msg_info "Applying DNS retry override in LXC (8.8.8.8, 1.1.1.1)"
      pct exec "$CTID" -- bash -c "printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >/etc/resolv.conf" >/dev/null 2>&1 || true
      msg_ok "DNS override applied in LXC"
    fi

    # Install SSH keys
    install_ssh_keys_into_ct

    # Start timer for duration tracking
    start_install_timer

    # Run application installer
    # Error handling already disabled above (before customization phase)

    # Signal handlers use this flag to stop the container on abort (SIGHUP/SIGINT/SIGTERM)
    # Without this, SSH disconnects leave the container running as an orphan process
    # that sends "configuring" status AFTER the host already reported "failed"
    export CONTAINER_INSTALLING=true

    lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
    local lxc_exit=$?

    unset CONTAINER_INSTALLING

    # Keep error handling DISABLED during failure detection and recovery
    # Re-enabling it here would cause any pct exec/pull failure to trigger
    # error_handler() on the host, bypassing the recovery menu entirely

    # Check for error flag file in container (more reliable than lxc-attach exit code)
    if [[ -n "${SESSION_ID:-}" ]]; then
      local error_flag="/root/.install-${SESSION_ID}.failed"
      if pct exec "$CTID" -- test -f "$error_flag" 2>/dev/null; then
        install_exit_code=$(pct exec "$CTID" -- cat "$error_flag" 2>/dev/null || echo "1")
        pct exec "$CTID" -- rm -f "$error_flag" 2>/dev/null || true
      fi
    fi

    # Fallback to lxc-attach exit code if no flag file
    if [[ $install_exit_code -eq 0 && ${lxc_exit:-0} -ne 0 ]]; then
      install_exit_code=${lxc_exit:-0}
    fi
  fi # end: if [[ $install_exit_code -eq 0 ]] (customization succeeded)

  # Installation or customization failed?
  if [[ $install_exit_code -ne 0 ]]; then
    # Prevent job-control signals from suspending the script during recovery.
    # In non-interactive shells (bash -c), background processes (spinner) can
    # trigger terminal-related signals that stop the entire process group.
    # TSTP = Ctrl+Z, TTIN = bg read from tty, TTOU = bg write to tty (tostop)
    trap '' TSTP TTIN TTOU

    msg_error "Installation failed in container ${CTID} (exit code: ${install_exit_code})"

    # Copy install log from container BEFORE API call so get_error_text() can read it
    local build_log_copied=false
    local install_log_copied=false
    local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"

    if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
      # Create combined log with header
      {
        echo "================================================================================"
        echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
        echo "Container ID: ${CTID}"
        echo "Session ID: ${SESSION_ID}"
        echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
        echo "================================================================================"
        echo ""
      } >"$combined_log"

      # Append BUILD_LOG (host-side creation log) if it exists
      if [[ -f "${BUILD_LOG}" ]]; then
        {
          echo "================================================================================"
          echo "PHASE 1: CONTAINER CREATION (Host)"
          echo "================================================================================"
          cat "${BUILD_LOG}"
          echo ""
        } >>"$combined_log"
        build_log_copied=true
      fi

      # Copy and append INSTALL_LOG from container
      local temp_install_log="/tmp/.install-temp-${SESSION_ID}.log"
      if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_install_log" 2>/dev/null; then
        {
          echo "================================================================================"
          echo "PHASE 2: APPLICATION INSTALLATION (Container)"
          echo "================================================================================"
          cat "$temp_install_log"
          echo ""
        } >>"$combined_log"
        rm -f "$temp_install_log"
        install_log_copied=true
        # Point INSTALL_LOG to combined log so get_full_log() finds it
        INSTALL_LOG="$combined_log"
      fi
    fi

    # Defense-in-depth: Ensure error handling stays disabled during recovery.
    # Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
    # and trap 'error_handler' ERR. If any code path above called such a function,
    # the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
    set +Eeuo pipefail
    trap - ERR

    # --- Exit code 1 subclassification: analyze logs BEFORE telemetry call ---
    # Exit code 1 is generic ("General error"). Analyze logs to determine the
    # real error category so telemetry gets a useful classification instead of "shell".
    local is_oom=false
    local is_network_issue=false
    local is_apt_issue=false
    local is_cmd_not_found=false
    local is_disk_full=false

    if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
      if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
        is_apt_issue=true
      fi
      if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
        is_oom=true
      fi
      if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
        is_network_issue=true
      fi
      if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
        is_cmd_not_found=true
      fi
      if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
        is_disk_full=true
      fi
    fi

    # Set override for categorize_error() so telemetry gets the real category
    if [[ "$is_apt_issue" == true ]]; then
      export ERROR_CATEGORY_OVERRIDE="dependency"
    elif [[ "$is_oom" == true ]]; then
      export ERROR_CATEGORY_OVERRIDE="resource"
    elif [[ "$is_network_issue" == true ]]; then
      export ERROR_CATEGORY_OVERRIDE="network"
    elif [[ "$is_disk_full" == true ]]; then
      export ERROR_CATEGORY_OVERRIDE="storage"
    elif [[ "$is_cmd_not_found" == true ]]; then
      export ERROR_CATEGORY_OVERRIDE="dependency"
    fi

    # Report failure to telemetry API (now with log available on host)
    # NOTE: Do NOT use msg_info/spinner here — the background spinner process
    # causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which
    # stops the entire process group and prevents the recovery dialog from appearing.
    $STD echo -e "${TAB}⏳ Reporting failure to telemetry..."
    post_update_to_api "failed" "$install_exit_code"
    $STD echo -e "${TAB}${CM:-✔} Failure reported"

    # Show combined log location
    if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
      msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
    fi

    # Dev mode: Keep container or open breakpoint shell
    if [[ "${DEV_MODE_KEEP:-false}" == "true" ]]; then
      msg_dev "Keep mode active - container ${CTID} preserved"
      return 0
    elif [[ "${DEV_MODE_BREAKPOINT:-false}" == "true" ]]; then
      msg_dev "Breakpoint mode - opening shell in container ${CTID}"
      echo -e "${YW}Type 'exit' to return to host${CL}"
      pct enter "$CTID"
      echo ""
      echo -en "${YW}Container ${CTID} still running. Remove now? (y/N): ${CL}"
      if read -r response </dev/tty && [[ "$response" =~ ^[Yy]$ ]]; then
        pct stop "$CTID" &>/dev/null || true
        pct destroy "$CTID" &>/dev/null || true
        msg_ok "Container ${CTID} removed"
      else
        msg_dev "Container ${CTID} kept for debugging"
      fi
      exit $install_exit_code
    fi

    # Prompt user for cleanup with 60s timeout
    echo ""

    # Extend error detection for non-exit-1 codes (exit 1 was already analyzed above)
    # The is_* flags were set above for exit code 1 log analysis; here we add
    # exit-code-specific detections for other codes.
    local error_explanation=""
    if declare -f explain_exit_code >/dev/null 2>&1; then
      error_explanation="$(explain_exit_code "$install_exit_code")"
    fi

    # OOM detection: exit codes 134 (SIGABRT/heap), 137 (SIGKILL/OOM), 243 (Node.js heap)
    if [[ $install_exit_code -eq 134 || $install_exit_code -eq 137 || $install_exit_code -eq 243 ]]; then
      is_oom=true
    fi

    # APT/DPKG detection: exit codes 100-102 (APT), 255 (DPKG with log evidence)
    case "$install_exit_code" in
    100 | 101 | 102) is_apt_issue=true ;;
    255)
      if [[ -f "$combined_log" ]] && grep -qiE 'dpkg|apt-get|apt\.conf|broken packages|unmet dependencies|E: Sub-process|E: Failed' "$combined_log"; then
        is_apt_issue=true
      fi
      ;;
    esac

    # Disk full / ENOSPC detection: errno -28 (ENOSPC), exit 228 (custom handler), exit 23 (curl write error)
    if [[ $install_exit_code -eq 228 || $install_exit_code -eq 23 ]]; then
      is_disk_full=true
    fi
    if [[ -f "$combined_log" ]] && grep -qiE 'ENOSPC|no space left on device|No space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
      is_disk_full=true
    fi

    # Command not found detection
    if [[ $install_exit_code -eq 127 ]]; then
      is_cmd_not_found=true
    fi

    # Network-related detection (curl/apt/git fetch failures and transient network issues)
    case "$install_exit_code" in
    6 | 7 | 22 | 28 | 35 | 52 | 56 | 57 | 75 | 78) is_network_issue=true ;;
    100)
      # APT can fail due to network (Failed to fetch)
      if [[ -f "$combined_log" ]] && grep -qiE 'Failed to fetch|Could not resolve|Connection failed|Network is unreachable|Temporary failure resolving' "$combined_log"; then
        is_network_issue=true
      fi
      ;;
    128)
      if [[ -f "$combined_log" ]] && grep -qiE 'RPC failed|early EOF|fetch-pack|HTTP/2 stream|Could not resolve host|Temporary failure resolving|Failed to fetch|Connection reset|Network is unreachable' "$combined_log"; then
        is_network_issue=true
      fi
      ;;
    esac

    # Show error explanation if available
    if [[ -n "$error_explanation" ]]; then
      echo -e "${TAB}${RD}Error: ${error_explanation}${CL}"
      echo ""
    fi

    # Show specific hints for known error types
    if [[ $install_exit_code -eq 10 ]]; then
      echo -e "${TAB}${INFO} This error usually means the container needs ${GN}privileged${CL} mode or Docker/nesting support."
      echo -e "${TAB}${INFO} Recreate with: Advanced Install → Container Type: ${GN}Privileged${CL}"
      echo ""
    fi

    if [[ $install_exit_code -eq 125 || $install_exit_code -eq 126 ]]; then
      echo -e "${TAB}${INFO} The command exists but cannot be executed. This may be a ${GN}permission${CL} issue."
      echo -e "${TAB}${INFO} If using Docker, ensure the container is ${GN}privileged${CL} or has correct permissions."
      echo ""
    fi

    if [[ "$is_disk_full" == true ]]; then
      echo -e "${TAB}${INFO} The container ran out of disk space during installation (${GN}ENOSPC${CL})."
      echo -e "${TAB}${INFO} Current disk size: ${GN}${DISK_SIZE} GB${CL}. A rebuild with doubled disk may resolve this."
      echo ""
    fi

    if [[ "$is_cmd_not_found" == true ]]; then
      local missing_cmd=""
      if [[ -f "$combined_log" ]]; then
        missing_cmd=$(grep -oiE '[a-zA-Z0-9_.-]+: command not found' "$combined_log" 2>/dev/null | tail -1 | sed 's/: command not found//') || true
      fi
      if [[ -n "$missing_cmd" ]]; then
        echo -e "${TAB}${INFO} Missing command: ${GN}${missing_cmd}${CL}"
      fi
      echo ""
    fi

    # Build recovery menu based on error type
    echo -e "${YW}What would you like to do?${CL}"
    echo ""
    echo -e "  ${GN}1)${CL} Remove container and exit"
    echo -e "  ${GN}2)${CL} Keep container for debugging"
    echo -e "  ${GN}3)${CL} Retry with verbose mode (full rebuild)"

    local next_option=4
    local APT_OPTION="" OOM_OPTION="" DNS_OPTION="" DISK_OPTION=""

    if [[ "$is_apt_issue" == true ]]; then
      if [[ "$var_os" == "alpine" ]]; then
        echo -e "  ${GN}${next_option})${CL} Repair APK state and re-run install (in-place)"
      else
        echo -e "  ${GN}${next_option})${CL} Repair APT/DPKG state and re-run install (in-place)"
      fi
      APT_OPTION=$next_option
      next_option=$((next_option + 1))
    fi

    if [[ "$is_oom" == true ]]; then
      local recovery_attempt="${RECOVERY_ATTEMPT:-0}"
      if [[ $recovery_attempt -lt 2 ]]; then
        local new_ram=$((RAM_SIZE * 2))
        local new_cpu=$((CORE_COUNT * 2))
        echo -e "  ${GN}${next_option})${CL} Retry with more resources (RAM: ${RAM_SIZE}→${new_ram} MiB, CPU: ${CORE_COUNT}→${new_cpu} cores)"
        OOM_OPTION=$next_option
        next_option=$((next_option + 1))
      else
        echo -e "  ${DGN}-)${CL} ${DGN}OOM retry exhausted (already retried ${recovery_attempt}x)${CL}"
      fi
    fi

    if [[ "$is_disk_full" == true ]]; then
      local disk_recovery_attempt="${DISK_RECOVERY_ATTEMPT:-0}"
      if [[ $disk_recovery_attempt -lt 2 ]]; then
        local new_disk=$((DISK_SIZE * 2))
        echo -e "  ${GN}${next_option})${CL} Retry with more disk space (Disk: ${DISK_SIZE}→${new_disk} GB)"
        DISK_OPTION=$next_option
        next_option=$((next_option + 1))
      else
        echo -e "  ${DGN}-)${CL} ${DGN}Disk resize retry exhausted (already retried ${disk_recovery_attempt}x)${CL}"
      fi
    fi

    if [[ "$is_network_issue" == true ]]; then
      echo -e "  ${GN}${next_option})${CL} Retry with DNS override in LXC (8.8.8.8 / 1.1.1.1)"
      DNS_OPTION=$next_option
      next_option=$((next_option + 1))
    fi

    local max_option=$((next_option - 1))

    echo ""
    echo -en "${YW}Select option [1-${max_option}] (default: 1, auto-remove in 60s): ${CL}"

    local response=""
    if read -t 60 -r response; then
      case "${response:-1}" in
      1)
        # Remove container
        echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
        pct stop "$CTID" &>/dev/null || true
        pct destroy "$CTID" &>/dev/null || true
        echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
        ;;
      2)
        echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}"
        # Dev mode: Setup MOTD/SSH for debugging access to broken container
        if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then
          echo -e "${TAB}${HOLD}${DGN}Setting up MOTD and SSH for debugging...${CL}"
          if pct exec "$CTID" -- bash -c "
              source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)
              declare -f motd_ssh >/dev/null 2>&1 && motd_ssh || true
            " >/dev/null 2>&1; then
            local ct_ip=$(pct exec "$CTID" ip a s dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
            echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
          fi
        fi
        exit $install_exit_code
        ;;
      3)
        # Retry with verbose mode (full rebuild)
        echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild...${CL}"
        pct stop "$CTID" &>/dev/null || true
        pct destroy "$CTID" &>/dev/null || true
        echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
        echo ""
        # Get new container ID
        local old_ctid="$CTID"
        export CTID=$(get_valid_container_id "$CTID")
        export VERBOSE="yes"
        export var_verbose="yes"

        # Show rebuild summary
        echo -e "${YW}Rebuilding with preserved settings:${CL}"
        echo -e "  Container ID: ${old_ctid} → ${CTID}"
        echo -e "  RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB"
        echo -e "  Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
        echo -e "  Verbose: ${GN}enabled${CL}"
        echo ""
        msg_info "Restarting installation..."
        # Re-run build_container
        build_container
        return $?
        ;;
      *)
        # Handle dynamic smart recovery options via named option variables
        local handled=false

        if [[ -n "${APT_OPTION}" && "${response}" == "${APT_OPTION}" ]]; then
          # Package manager in-place repair: fix broken state and re-run install script
          handled=true
          if [[ "$var_os" == "alpine" ]]; then
            echo -e "\n${TAB}${HOLD}${YW}Repairing APK state in container ${CTID}...${CL}"
            pct exec "$CTID" -- ash -c "
              apk fix 2>/dev/null || true
              apk cache clean 2>/dev/null || true
              apk update 2>/dev/null || true
            " >/dev/null 2>&1 || true
            echo -e "${BFR}${CM}${GN}APK state repaired in container ${CTID}${CL}"
          else
            echo -e "\n${TAB}${HOLD}${YW}Repairing APT/DPKG state in container ${CTID}...${CL}"
            pct exec "$CTID" -- bash -c "
              DEBIAN_FRONTEND=noninteractive dpkg --configure -a 2>/dev/null || true
              apt-get -f install -y 2>/dev/null || true
              apt-get clean 2>/dev/null
              apt-get update 2>/dev/null || true
            " >/dev/null 2>&1 || true
            echo -e "${BFR}${CM}${GN}APT/DPKG state repaired in container ${CTID}${CL}"
          fi
          echo ""
          export VERBOSE="yes"
          export var_verbose="yes"

          echo -e "${YW}Re-running installation in existing container ${CTID}:${CL}"
          echo -e "  RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores | Disk: ${DISK_SIZE} GB"
          echo -e "  Verbose: ${GN}enabled${CL}"
          echo ""
          msg_info "Re-running installation script..."

          # Re-run install script in existing container (don't destroy/recreate)
          set +Eeuo pipefail
          trap - ERR
          lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
          local apt_retry_exit=$?
          set -Eeuo pipefail
          trap 'error_handler' ERR

          # Check for error flag from retry
          local apt_retry_code=0
          if [[ -n "${SESSION_ID:-}" ]]; then
            local retry_error_flag="/root/.install-${SESSION_ID}.failed"
            if pct exec "$CTID" -- test -f "$retry_error_flag" 2>/dev/null; then
              apt_retry_code=$(pct exec "$CTID" -- cat "$retry_error_flag" 2>/dev/null || echo "1")
              pct exec "$CTID" -- rm -f "$retry_error_flag" 2>/dev/null || true
            fi
          fi

          if [[ $apt_retry_code -eq 0 && $apt_retry_exit -ne 0 ]]; then
            apt_retry_code=$apt_retry_exit
          fi

          if [[ $apt_retry_code -eq 0 ]]; then
            msg_ok "Installation completed successfully after APT repair!"
            INSTALL_COMPLETE=true
            post_update_to_api "done" "0" "force"
            return 0
          else
            msg_error "Installation still failed after APT repair (exit code: ${apt_retry_code})"
            install_exit_code=$apt_retry_code
          fi
        fi

        if [[ -n "${OOM_OPTION}" && "${response}" == "${OOM_OPTION}" ]]; then
          # Retry with doubled resources
          handled=true
          echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more resources...${CL}"
          pct stop "$CTID" &>/dev/null || true
          pct destroy "$CTID" &>/dev/null || true
          echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
          echo ""
          local old_ctid="$CTID"
          local old_ram="$RAM_SIZE"
          local old_cpu="$CORE_COUNT"
          export CTID=$(get_valid_container_id "$CTID")
          export RAM_SIZE=$((RAM_SIZE * 2))
          export CORE_COUNT=$((CORE_COUNT * 2))
          export var_ram="$RAM_SIZE"
          export var_cpu="$CORE_COUNT"
          export VERBOSE="yes"
          export var_verbose="yes"
          export RECOVERY_ATTEMPT=$((${RECOVERY_ATTEMPT:-0} + 1))

          echo -e "${YW}Rebuilding with increased resources (attempt ${RECOVERY_ATTEMPT}/2):${CL}"
          echo -e "  Container ID: ${old_ctid} → ${CTID}"
          echo -e "  RAM: ${old_ram} → ${GN}${RAM_SIZE}${CL} MiB (x2)"
          echo -e "  CPU: ${old_cpu} → ${GN}${CORE_COUNT}${CL} cores (x2)"
          echo -e "  Disk: ${DISK_SIZE} GB | Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
          echo -e "  Verbose: ${GN}enabled${CL}"
          echo ""
          msg_info "Restarting installation..."
          build_container
          return $?
        fi

        if [[ -n "${DISK_OPTION}" && "${response}" == "${DISK_OPTION}" ]]; then
          # Retry with doubled disk size
          handled=true
          echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with more disk space...${CL}"
          pct stop "$CTID" &>/dev/null || true
          pct destroy "$CTID" &>/dev/null || true
          echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
          echo ""
          local old_ctid="$CTID"
          local old_disk="$DISK_SIZE"
          export CTID=$(get_valid_container_id "$CTID")
          export DISK_SIZE=$((DISK_SIZE * 2))
          export var_disk="$DISK_SIZE"
          export VERBOSE="yes"
          export var_verbose="yes"
          export DISK_RECOVERY_ATTEMPT=$((${DISK_RECOVERY_ATTEMPT:-0} + 1))

          echo -e "${YW}Rebuilding with increased disk space (attempt ${DISK_RECOVERY_ATTEMPT}/2):${CL}"
          echo -e "  Container ID: ${old_ctid} → ${CTID}"
          echo -e "  Disk: ${old_disk} → ${GN}${DISK_SIZE}${CL} GB (x2)"
          echo -e "  RAM: ${RAM_SIZE} MiB | CPU: ${CORE_COUNT} cores"
          echo -e "  Network: ${NET:-dhcp} | Bridge: ${BRG:-vmbr0}"
          echo -e "  Verbose: ${GN}enabled${CL}"
          echo ""
          msg_info "Restarting installation..."
          build_container
          return $?
        fi

        if [[ -n "${DNS_OPTION}" && "${response}" == "${DNS_OPTION}" ]]; then
          # Retry with DNS override in LXC
          handled=true
          echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID} for rebuild with DNS override...${CL}"
          pct stop "$CTID" &>/dev/null || true
          pct destroy "$CTID" &>/dev/null || true
          echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
          echo ""
          local old_ctid="$CTID"
          export CTID=$(get_valid_container_id "$CTID")
          export DNS_RETRY_OVERRIDE="true"
          export VERBOSE="yes"
          export var_verbose="yes"

          echo -e "${YW}Rebuilding with DNS override in LXC:${CL}"
          echo -e "  Container ID: ${old_ctid} → ${CTID}"
          echo -e "  DNS: ${GN}8.8.8.8, 1.1.1.1${CL} (inside LXC only)"
          echo -e "  Verbose: ${GN}enabled${CL}"
          echo ""
          msg_info "Restarting installation..."
          build_container
          return $?
        fi

        if [[ "$handled" == false ]]; then
          echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
          exit $install_exit_code
        fi
        ;;
      esac
    else
      # Timeout - auto-remove
      echo ""
      msg_info "No response - removing container ${CTID}"
      pct stop "$CTID" &>/dev/null || true
      pct destroy "$CTID" &>/dev/null || true
      msg_ok "Container ${CTID} removed"
    fi

    # Force one final status update attempt after cleanup
    # This ensures status is updated even if the first attempt failed (e.g., HTTP 400)
    $STD echo -e "${TAB}⏳ Finalizing telemetry report..."
    post_update_to_api "failed" "$install_exit_code" "force"
    $STD echo -e "${TAB}${CM:-✔} Telemetry finalized"

    # Restore default job-control signal handling before exit
    trap - TSTP TTIN TTOU
    exit $install_exit_code
  fi

  # Re-enable error handling after successful install or recovery menu completion
  set -Eeuo pipefail
  trap 'error_handler' ERR
}

destroy_lxc() {
  if [[ -z "$CT_ID" ]]; then
    msg_error "No CT_ID found. Nothing to remove."
    return 65
  fi

  # Abort on Ctrl-C / Ctrl-D / ESC
  trap 'echo; msg_error "Aborted by user (SIGINT/SIGQUIT)"; return 130' INT QUIT

  local prompt
  if ! read -rp "Remove this Container? <y/N> " prompt </dev/tty; then
    # read returns non-zero on Ctrl-D/ESC
    msg_error "Aborted input (Ctrl-D/ESC)"
    return 130
  fi

  case "${prompt,,}" in
  y | yes)
    if pct stop "$CT_ID" &>/dev/null && pct destroy "$CT_ID" &>/dev/null; then
      msg_ok "Removed Container $CT_ID"
    else
      msg_error "Failed to remove Container $CT_ID"
      return 1
    fi
    ;;
  "" | n | no)
    msg_custom "ℹ️" "${BL}" "Container was not removed."
    ;;
  *)
    msg_warn "Invalid response. Container was not removed."
    ;;
  esac
}

# ------------------------------------------------------------------------------
# Storage discovery / selection helpers
# ------------------------------------------------------------------------------
resolve_storage_preselect() {
  local class="$1" preselect="$2" required_content=""
  case "$class" in
  template) required_content="vztmpl" ;;
  container) required_content="rootdir" ;;
  *) return 65 ;;
  esac
  [[ -z "$preselect" ]] && return 1
  if ! pvesm status -content "$required_content" | awk 'NR>1{print $1}' | grep -qx -- "$preselect"; then
    msg_warn "Preselected storage '${preselect}' does not support content '${required_content}' (or not found)"
    return 238
  fi

  local line total used free
  line="$(pvesm status | awk -v s="$preselect" 'NR>1 && $1==s {print $0}')"
  if [[ -z "$line" ]]; then
    STORAGE_INFO="n/a"
  else
    total="$(awk '{print $4}' <<<"$line")"
    used="$(awk '{print $5}' <<<"$line")"
    free="$(awk '{print $6}' <<<"$line")"
    local total_h used_h free_h
    if command -v numfmt >/dev/null 2>&1; then
      total_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")"
      used_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")"
      free_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")"
      STORAGE_INFO="Free: ${free_h}  Used: ${used_h}"
    else
      STORAGE_INFO="Free: ${free}  Used: ${used}"
    fi
  fi
  STORAGE_RESULT="$preselect"
  return 0
}

fix_gpu_gids() {
  if [[ -z "${GPU_TYPE:-}" ]]; then
    return 0
  fi

  msg_info "Detecting and setting correct GPU group IDs"

  # Get actual GIDs from container
  local video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
  local render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")

  # Create groups if they don't exist
  if [[ -z "$video_gid" ]]; then
    pct exec "$CTID" -- sh -c "groupadd -r video 2>/dev/null || true" >/dev/null 2>&1
    video_gid=$(pct exec "$CTID" -- sh -c "getent group video 2>/dev/null | cut -d: -f3")
    [[ -z "$video_gid" ]] && video_gid="44"
  fi

  if [[ -z "$render_gid" ]]; then
    pct exec "$CTID" -- sh -c "groupadd -r render 2>/dev/null || true" >/dev/null 2>&1
    render_gid=$(pct exec "$CTID" -- sh -c "getent group render 2>/dev/null | cut -d: -f3")
    [[ -z "$render_gid" ]] && render_gid="104"
  fi

  # Stop container to update config
  pct stop "$CTID" >/dev/null 2>&1
  sleep 1

  # Validate GIDs are numeric before sed
  [[ "$render_gid" =~ ^[0-9]+$ ]] || render_gid="104"
  [[ "$video_gid" =~ ^[0-9]+$ ]] || video_gid="44"

  # Update dev entries with correct GIDs
  sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG"
  sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG"

  # Restart container
  pct start "$CTID" >/dev/null 2>&1
  sleep 2

  msg_ok "GPU passthrough configured (video:${video_gid}, render:${render_gid})"

  # For privileged containers: also fix permissions inside container
  if [[ "$CT_TYPE" == "0" ]]; then
    pct exec "$CTID" -- sh -c "
      if [ -d /dev/dri ]; then
        for dev in /dev/dri/*; do
          if [ -e \"\$dev\" ]; then
            case \"\$dev\" in
              *renderD*) chgrp ${render_gid} \"\$dev\" 2>/dev/null || true ;;
              *)         chgrp ${video_gid} \"\$dev\" 2>/dev/null || true ;;
            esac
            chmod 660 \"\$dev\" 2>/dev/null || true
          fi
        done
      fi
    " >/dev/null 2>&1
  fi
}

check_storage_support() {
  local CONTENT="$1" VALID=0
  while IFS= read -r line; do
    local STORAGE_NAME
    STORAGE_NAME=$(awk '{print $1}' <<<"$line")
    [[ -n "$STORAGE_NAME" ]] && VALID=1
  done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
  [[ $VALID -eq 1 ]]
}

select_storage() {
  local CLASS=$1 CONTENT CONTENT_LABEL
  case $CLASS in
  container)
    CONTENT='rootdir'
    CONTENT_LABEL='Container'
    ;;
  template)
    CONTENT='vztmpl'
    CONTENT_LABEL='Container template'
    ;;
  iso)
    CONTENT='iso'
    CONTENT_LABEL='ISO image'
    ;;
  images)
    CONTENT='images'
    CONTENT_LABEL='VM Disk image'
    ;;
  backup)
    CONTENT='backup'
    CONTENT_LABEL='Backup'
    ;;
  snippets)
    CONTENT='snippets'
    CONTENT_LABEL='Snippets'
    ;;
  *)
    msg_error "Invalid storage class '$CLASS'"
    return 65
    ;;
  esac

  declare -A STORAGE_MAP
  local -a MENU=()
  local COL_WIDTH=0

  while read -r TAG TYPE _ TOTAL USED FREE _; do
    [[ -n "$TAG" && -n "$TYPE" ]] || continue
    local DISPLAY="${TAG} (${TYPE})"
    local USED_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$USED")
    local FREE_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$FREE")
    local INFO="Free: ${FREE_FMT}B  Used: ${USED_FMT}B"
    STORAGE_MAP["$DISPLAY"]="$TAG"
    MENU+=("$DISPLAY" "$INFO" "OFF")
    ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
  done < <(pvesm status -content "$CONTENT" | awk 'NR>1')

  if [[ ${#MENU[@]} -eq 0 ]]; then
    msg_error "No storage found for content type '$CONTENT'."
    return 2
  fi

  if [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
    STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
    STORAGE_INFO="${MENU[1]}"
    return 0
  fi

  local WIDTH=$((COL_WIDTH + 42))
  while true; do
    local DISPLAY_SELECTED
    DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
      --title "Storage Pools" \
      --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
      16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3) || { exit_script; }

    DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
    if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
      whiptail --msgbox "No valid storage selected. Please try again." 8 58
      continue
    fi
    STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
    for ((i = 0; i < ${#MENU[@]}; i += 3)); do
      if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
        STORAGE_INFO="${MENU[$i + 1]}"
        break
      fi
    done

    # Validate storage space for container storage
    if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then
      validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes"
      # Continue even if validation fails - user was warned
    fi

    return 0
  done
}

# ------------------------------------------------------------------------------
# validate_storage_space()
#
# - Validates if storage has enough free space for container
# - Takes storage name and required size in GB
# - Returns 0 if enough space, 1 if not enough, 2 if storage unavailable
# - Can optionally show whiptail warning
# - Handles all storage types: dir, lvm, lvmthin, zfs, nfs, cifs, etc.
# ------------------------------------------------------------------------------
validate_storage_space() {
  local storage="$1"
  local required_gb="${2:-8}"
  local show_dialog="${3:-no}"

  # Get full storage line from pvesm status
  local storage_line
  storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}')

  # Check if storage exists and is active
  if [[ -z "$storage_line" ]]; then
    [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️  Warning: Storage '$storage' not found!\n\nThe storage may be unavailable or disabled." 10 60
    return 236
  fi

  # Check storage status (column 3)
  local status
  status=$(awk '{print $3}' <<<"$storage_line")
  if [[ "$status" == "disabled" ]]; then
    [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️  Warning: Storage '$storage' is disabled!\n\nPlease enable the storage first." 10 60
    return 236
  fi

  # Get storage type and free space (column 6)
  local storage_type storage_free
  storage_type=$(awk '{print $2}' <<<"$storage_line")
  storage_free=$(awk '{print $6}' <<<"$storage_line")

  # Some storage types (like PBS, iSCSI) don't report size info
  # In these cases, skip space validation
  if [[ -z "$storage_free" || "$storage_free" == "0" ]]; then
    # Silent pass for storages without size info
    return 0
  fi

  local required_kb=$((required_gb * 1024 * 1024))
  local free_gb_fmt
  free_gb_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$storage_free" 2>/dev/null || echo "${storage_free}KB")

  if [[ "$storage_free" -lt "$required_kb" ]]; then
    if [[ "$show_dialog" == "yes" ]]; then
      whiptail --msgbox "⚠️  Warning: Storage '$storage' may not have enough space!\n\nStorage Type: ${storage_type}\nRequired: ${required_gb}GB\nAvailable: ${free_gb_fmt}\n\nYou can continue, but creation might fail." 14 70
    fi
    return 236
  fi

  return 0
}

# ==============================================================================
# SECTION 8: CONTAINER CREATION
# ==============================================================================

# ------------------------------------------------------------------------------
# create_lxc_container()
#
# - Main function for creating LXC containers
# - Handles all phases: validation, template discovery, container creation,
#   network config, storage, etc.
# - Extensive error checking with detailed exit codes
# ------------------------------------------------------------------------------
create_lxc_container() {
  # ------------------------------------------------------------------------------
  # Optional verbose mode (debug tracing)
  # ------------------------------------------------------------------------------
  if [[ "${CREATE_LXC_VERBOSE:-no}" == "yes" ]]; then set -x; fi

  # ------------------------------------------------------------------------------
  # Helpers (dynamic versioning / template parsing)
  # ------------------------------------------------------------------------------
  pkg_ver() { dpkg-query -W -f='${Version}\n' "$1" 2>/dev/null || echo ""; }
  pkg_cand() { apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/ {print $2}'; }

  ver_ge() { dpkg --compare-versions "$1" ge "$2"; }
  ver_gt() { dpkg --compare-versions "$1" gt "$2"; }
  ver_lt() { dpkg --compare-versions "$1" lt "$2"; }

  # Extract Debian OS minor from template name: debian-13-standard_13.1-1_amd64.tar.zst => "13.1"
  parse_template_osver() { sed -n 's/.*_\([0-9][0-9]*\(\.[0-9]\+\)\?\)-.*/\1/p' <<<"$1"; }

  # Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
  # Returns:
  #   0 = no upgrade needed
  #   1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
  #   2 = user declined
  #   3 = upgrade attempted but failed OR retry failed
  offer_lxc_stack_upgrade_and_maybe_retry() {
    local do_retry="${1:-no}" # yes|no
    local _pvec_i _pvec_c _lxcp_i _lxcp_c need=0

    _pvec_i="$(pkg_ver pve-container)"
    _lxcp_i="$(pkg_ver lxc-pve)"
    _pvec_c="$(pkg_cand pve-container)"
    _lxcp_c="$(pkg_cand lxc-pve)"

    if [[ -n "$_pvec_c" && "$_pvec_c" != "none" ]]; then
      ver_gt "$_pvec_c" "${_pvec_i:-0}" && need=1
    fi
    if [[ -n "$_lxcp_c" && "$_lxcp_c" != "none" ]]; then
      ver_gt "$_lxcp_c" "${_lxcp_i:-0}" && need=1
    fi
    if [[ $need -eq 0 ]]; then
      msg_debug "No newer candidate for pve-container/lxc-pve (installed=$_pvec_i/$_lxcp_i, cand=$_pvec_c/$_lxcp_c)"
      return 0
    fi

    msg_warn "An update for the Proxmox LXC stack is available"
    echo "  pve-container: installed=${_pvec_i:-n/a}  candidate=${_pvec_c:-n/a}"
    echo "  lxc-pve     : installed=${_lxcp_i:-n/a}  candidate=${_lxcp_c:-n/a}"
    echo
    read -rp "Do you want to upgrade now? [y/N] " _ans </dev/tty
    case "${_ans,,}" in
    y | yes)
      msg_info "Upgrading Proxmox LXC stack (pve-container, lxc-pve)"
      apt_update_safe
      if $STD apt-get install -y --only-upgrade pve-container lxc-pve; then
        msg_ok "LXC stack upgraded."
        if [[ "$do_retry" == "yes" ]]; then
          msg_info "Retrying container creation after upgrade"
          if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
            msg_ok "Container created successfully after upgrade."
            return 0
          else
            msg_error "pct create still failed after upgrade. See $LOGFILE"
            return 3
          fi
        fi
        return 1
      else
        msg_error "Upgrade failed. Please check APT output."
        return 3
      fi
      ;;
    *) return 2 ;;
    esac
  }

  # ------------------------------------------------------------------------------
  # Required input variables
  # ------------------------------------------------------------------------------
  [[ "${CTID:-}" ]] || {
    msg_error "You need to set 'CTID' variable."
    exit 203
  }
  [[ "${PCT_OSTYPE:-}" ]] || {
    msg_error "You need to set 'PCT_OSTYPE' variable."
    exit 204
  }

  msg_debug "CTID=$CTID"
  msg_debug "PCT_OSTYPE=$PCT_OSTYPE"
  msg_debug "PCT_OSVERSION=${PCT_OSVERSION:-default}"

  # ID checks
  [[ "$CTID" -ge 100 ]] || {
    msg_error "ID cannot be less than 100."
    exit 205
  }
  if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
    msg_warn "Container/VM ID $CTID is already in use (detected late). Reassigning..."
    CTID=$(get_valid_container_id "$((CTID + 1))")
    export CTID
    msg_ok "Reassigned to container ID $CTID"
  fi

  # Report installation start to API early - captures failures in storage/template/create
  post_to_api

  # Transition to 'validation' — Proxmox-internal checks (storage, template, cluster)
  post_progress_to_api "validation"

  # Storage capability check
  check_storage_support "rootdir" || {
    msg_error "No valid storage found for 'rootdir' [Container]"
    exit 119
  }
  check_storage_support "vztmpl" || {
    msg_error "No valid storage found for 'vztmpl' [Template]"
    exit 120
  }

  # Template storage selection
  if resolve_storage_preselect template "${TEMPLATE_STORAGE:-}"; then
    TEMPLATE_STORAGE="$STORAGE_RESULT"
    TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
    msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
  else
    while true; do
      if [[ -z "${var_template_storage:-}" ]]; then
        if select_storage template; then
          TEMPLATE_STORAGE="$STORAGE_RESULT"
          TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
          msg_ok "Storage ${BL}${TEMPLATE_STORAGE}${CL} (${TEMPLATE_STORAGE_INFO}) [Template]"
          break
        fi
      fi
    done
  fi

  # Container storage selection
  if resolve_storage_preselect container "${CONTAINER_STORAGE:-}"; then
    CONTAINER_STORAGE="$STORAGE_RESULT"
    CONTAINER_STORAGE_INFO="$STORAGE_INFO"
    msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
  else
    if [[ -z "${var_container_storage:-}" ]]; then
      if select_storage container; then
        CONTAINER_STORAGE="$STORAGE_RESULT"
        CONTAINER_STORAGE_INFO="$STORAGE_INFO"
        msg_ok "Storage ${BL}${CONTAINER_STORAGE}${CL} (${CONTAINER_STORAGE_INFO}) [Container]"
      fi
    fi
  fi

  msg_info "Validating storage '$CONTAINER_STORAGE'"
  STORAGE_TYPE=$(grep -E "^[^:]+: $CONTAINER_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 | head -1 || true)

  if [[ -z "$STORAGE_TYPE" ]]; then
    msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg"
    exit 213
  fi

  case "$STORAGE_TYPE" in
  iscsidirect)
    msg_error "Storage '$CONTAINER_STORAGE' uses iSCSI-direct which does not support container rootfs."
    exit 212
    ;;
  iscsi | zfs)
    msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support container rootdir content."
    exit 213
    ;;
  cephfs)
    msg_error "Storage '$CONTAINER_STORAGE' uses CephFS which is not supported for LXC rootfs."
    exit 219
    ;;
  pbs)
    msg_error "Storage '$CONTAINER_STORAGE' is a Proxmox Backup Server — cannot be used for containers."
    exit 224
    ;;
  linstor | rbd | nfs | cifs)
    if ! pvesm status -storage "$CONTAINER_STORAGE" &>/dev/null; then
      msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) is not accessible or inactive."
      exit 217
    fi
    ;;
  esac

  if ! pvesm status -content rootdir 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CONTAINER_STORAGE"; then
    msg_error "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) does not support 'rootdir' content."
    exit 213
  fi
  msg_ok "Storage '$CONTAINER_STORAGE' ($STORAGE_TYPE) validated"

  msg_info "Validating template storage '$TEMPLATE_STORAGE'"
  TEMPLATE_TYPE=$(grep -E "^[^:]+: $TEMPLATE_STORAGE$" /etc/pve/storage.cfg | cut -d: -f1 || true)

  if ! pvesm status -content vztmpl 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$TEMPLATE_STORAGE"; then
    msg_warn "Template storage '$TEMPLATE_STORAGE' may not support 'vztmpl'"
  fi
  msg_ok "Template storage '$TEMPLATE_STORAGE' validated"

  # Cluster quorum (if cluster)
  if [[ -f /etc/pve/corosync.conf ]]; then
    msg_info "Checking cluster quorum"
    if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
      msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
      exit 210
    fi
    msg_ok "Cluster is quorate"
  fi

  # ------------------------------------------------------------------------------
  # Template discovery & validation
  # ------------------------------------------------------------------------------
  TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
  case "$PCT_OSTYPE" in
  debian | ubuntu) TEMPLATE_PATTERN="-standard_" ;;
  alpine | fedora | rocky | centos) TEMPLATE_PATTERN="-default_" ;;
  *) TEMPLATE_PATTERN="" ;;
  esac

  msg_info "Searching for template '$TEMPLATE_SEARCH'"

  # Initialize variables
  ONLINE_TEMPLATE=""
  ONLINE_TEMPLATES=()

  # Step 1: Check local templates first (instant)
  mapfile -t LOCAL_TEMPLATES < <(
    pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
      awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
      sed 's|.*/||' | sort -t - -k 2 -V
  )

  # Step 2: If local template found, use it immediately (skip pveam update)
  if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
    TEMPLATE="${LOCAL_TEMPLATES[-1]}"
    TEMPLATE_SOURCE="local"
    msg_ok "Template search completed"
  else
    # Step 3: No local template - need to check online (this may be slow)
    msg_info "No local template found, checking online catalog..."

    # Update catalog with timeout to prevent long hangs
    if command -v timeout &>/dev/null; then
      if ! timeout 30 pveam update >/dev/null 2>&1; then
        msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose."
      fi
    else
      pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)"
    fi

    ONLINE_TEMPLATES=()
    mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true)
    [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"

    TEMPLATE="$ONLINE_TEMPLATE"
    TEMPLATE_SOURCE="online"
    msg_ok "Template search completed"
  fi

  # If still no template, try to find alternatives
  if [[ -z "$TEMPLATE" ]]; then
    msg_warn "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..."

    # Get all available versions for this OS type
    AVAILABLE_VERSIONS=()
    mapfile -t AVAILABLE_VERSIONS < <(
      pveam available -section system 2>/dev/null |
        grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
        awk -F'\t' '{print $1}' |
        grep "^${PCT_OSTYPE}-" |
        sed -E "s/.*${PCT_OSTYPE}-([0-9]+(\.[0-9]+)?).*/\1/" |
        sort -u -V 2>/dev/null
    )

    if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
      echo ""
      echo "${BL}Available ${PCT_OSTYPE} versions:${CL}"
      for i in "${!AVAILABLE_VERSIONS[@]}"; do
        echo "  [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
      done
      echo ""
      read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or press Enter to cancel: " choice </dev/tty

      if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
        PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}"
        TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}"

        ONLINE_TEMPLATES=()
        mapfile -t ONLINE_TEMPLATES < <(
          pveam available -section system 2>/dev/null |
            grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
            awk '{print $2}' |
            grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" |
            sort -t - -k 2 -V 2>/dev/null || true
        )

        if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then
          TEMPLATE="${ONLINE_TEMPLATES[-1]}"
          TEMPLATE_SOURCE="online"
        else
          msg_error "No templates available for ${PCT_OSTYPE} ${PCT_OSVERSION}"
          exit 225
        fi
      else
        msg_custom "🚫" "${YW}" "Installation cancelled"
        exit 0
      fi
    else
      msg_error "No ${PCT_OSTYPE} templates available at all"
      exit 225
    fi
  fi

  TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
  if [[ -z "$TEMPLATE_PATH" ]]; then
    TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
    [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
  fi

  # If we still don't have a path but have a valid template name, construct it
  if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
    TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
  fi

  [[ -n "$TEMPLATE_PATH" ]] || {
    if [[ -z "$TEMPLATE" ]]; then
      msg_error "Template ${PCT_OSTYPE} ${PCT_OSVERSION} not available"

      # Get available versions
      mapfile -t AVAILABLE_VERSIONS < <(
        pveam available -section system 2>/dev/null |
          grep "^${PCT_OSTYPE}-" |
          sed -E 's/.*'"${PCT_OSTYPE}"'-([0-9]+\.[0-9]+).*/\1/' |
          grep -E '^[0-9]+\.[0-9]+$' |
          sort -u -V 2>/dev/null || sort -u
      )

      if [[ ${#AVAILABLE_VERSIONS[@]} -gt 0 ]]; then
        echo -e "\n${BL}Available versions:${CL}"
        for i in "${!AVAILABLE_VERSIONS[@]}"; do
          echo "  [$((i + 1))] ${AVAILABLE_VERSIONS[$i]}"
        done

        echo ""
        read -p "Select version [1-${#AVAILABLE_VERSIONS[@]}] or Enter to exit: " choice </dev/tty

        if [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -ge 1 ]] && [[ "$choice" -le ${#AVAILABLE_VERSIONS[@]} ]]; then
          export var_version="${AVAILABLE_VERSIONS[$((choice - 1))]}"
          export PCT_OSVERSION="$var_version"
          msg_ok "Switched to ${PCT_OSTYPE} ${var_version}"

          # Retry template search with new version
          TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"

          mapfile -t LOCAL_TEMPLATES < <(
            pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
              awk -v search="${TEMPLATE_SEARCH}-" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' |
              sed 's|.*/||' | sort -t - -k 2 -V
          )
          mapfile -t ONLINE_TEMPLATES < <(
            pveam available -section system 2>/dev/null |
              grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' |
              awk '{print $2}' |
              grep -E "^${TEMPLATE_SEARCH}-.*${TEMPLATE_PATTERN}" |
              sort -t - -k 2 -V 2>/dev/null || true
          )
          ONLINE_TEMPLATE=""
          [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}"

          if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then
            TEMPLATE="${LOCAL_TEMPLATES[-1]}"
            TEMPLATE_SOURCE="local"
          else
            TEMPLATE="$ONLINE_TEMPLATE"
            TEMPLATE_SOURCE="online"
          fi

          TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
          if [[ -z "$TEMPLATE_PATH" ]]; then
            TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
            [[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
          fi

          # If we still don't have a path but have a valid template name, construct it
          if [[ -z "$TEMPLATE_PATH" && -n "$TEMPLATE" ]]; then
            TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
          fi

          [[ -n "$TEMPLATE_PATH" ]] || {
            msg_error "Template still not found after version change"
            exit 220
          }
        else
          msg_custom "🚫" "${YW}" "Installation cancelled"
          exit 0
        fi
      else
        msg_error "No ${PCT_OSTYPE} templates available"
        exit 220
      fi
    fi
  }

  # Validate that we found a template
  if [[ -z "$TEMPLATE" ]]; then
    msg_error "No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}"
    msg_custom "ℹ️" "${YW}" "Please check:"
    msg_custom "  •" "${YW}" "Is pveam catalog available? (run: pveam available -section system)"
    msg_custom "  •" "${YW}" "Does the template exist for your OS version?"
    exit 225
  fi

  msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
  msg_debug "Resolved TEMPLATE_PATH=$TEMPLATE_PATH"

  NEED_DOWNLOAD=0
  if [[ ! -f "$TEMPLATE_PATH" ]]; then
    msg_info "Template not present locally – will download."
    NEED_DOWNLOAD=1
  elif [[ ! -r "$TEMPLATE_PATH" ]]; then
    msg_error "Template file exists but is not readable – check permissions."
    exit 221
  elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
    if [[ -n "$ONLINE_TEMPLATE" ]]; then
      msg_warn "Template file too small (<1MB) – re-downloading."
      NEED_DOWNLOAD=1
    else
      msg_warn "Template looks too small, but no online version exists. Keeping local file."
    fi
  elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
    if [[ -n "$ONLINE_TEMPLATE" ]]; then
      msg_warn "Template appears corrupted – re-downloading."
      NEED_DOWNLOAD=1
    else
      msg_warn "Template appears corrupted, but no online version exists. Keeping local file."
    fi
  else
    $STD msg_ok "Template $TEMPLATE is present and valid."
  fi

  if [[ "$TEMPLATE_SOURCE" == "local" && -n "$ONLINE_TEMPLATE" && "$TEMPLATE" != "$ONLINE_TEMPLATE" ]]; then
    msg_warn "Local template is outdated: $TEMPLATE (latest available: $ONLINE_TEMPLATE)"
    if whiptail --yesno "A newer template is available:\n$ONLINE_TEMPLATE\n\nDo you want to download and use it instead?" 12 70; then
      TEMPLATE="$ONLINE_TEMPLATE"
      NEED_DOWNLOAD=1
    else
      msg_custom "ℹ️" "${BL}" "Continuing with local template $TEMPLATE"
    fi
  fi

  if [[ "$NEED_DOWNLOAD" -eq 1 ]]; then
    [[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
    for attempt in {1..3}; do
      msg_info "Attempt $attempt: Downloading template $TEMPLATE to $TEMPLATE_STORAGE"
      if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1; then
        msg_ok "Template download successful."
        break
      fi
      if [[ $attempt -eq 3 ]]; then
        msg_error "Failed after 3 attempts. Please check network access, permissions, or manually run:\n  pveam download $TEMPLATE_STORAGE $TEMPLATE"
        exit 222
      fi
      sleep $((attempt * 5))
    done
  fi

  if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
    msg_error "Template $TEMPLATE not available in storage $TEMPLATE_STORAGE after download."
    exit 223
  fi

  # ------------------------------------------------------------------------------
  # Dynamic preflight for Debian 13.x: offer upgrade if available (no hard mins)
  # ------------------------------------------------------------------------------
  if [[ "$PCT_OSTYPE" == "debian" ]]; then
    OSVER="$(parse_template_osver "$TEMPLATE")"
    if [[ -n "$OSVER" ]]; then
      offer_lxc_stack_upgrade_and_maybe_retry "no" || true
    fi
  fi

  # ------------------------------------------------------------------------------
  # Create LXC Container
  # ------------------------------------------------------------------------------
  msg_info "Creating LXC container"

  # Ensure subuid/subgid entries exist
  grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
  grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid

  # PCT_OPTIONS is now a string (exported from build_container)
  # Add rootfs if not already specified
  if [[ ! "$PCT_OPTIONS" =~ "-rootfs" ]]; then
    PCT_OPTIONS="$PCT_OPTIONS
  -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}"
  fi

  # Lock by template file (avoid concurrent template downloads/validation)
  lockfile="/tmp/template.${TEMPLATE}.lock"

  # Cleanup stale lock files (older than 1 hour - likely from crashed processes)
  if [[ -f "$lockfile" ]]; then
    local lock_age=$(($(date +%s) - $(stat -c %Y "$lockfile" 2>/dev/null || echo 0)))
    if [[ $lock_age -gt 3600 ]]; then
      msg_warn "Removing stale template lock file (age: ${lock_age}s)"
      rm -f "$lockfile"
    fi
  fi

  exec 9>"$lockfile" || {
    msg_error "Failed to create lock file '$lockfile'."
    exit 200
  }

  # Retry logic for template lock (another container creation may be running)
  local lock_attempts=0
  local max_lock_attempts=10
  local lock_wait_time=30

  while ! flock -w "$lock_wait_time" 9; do
    lock_attempts=$((lock_attempts + 1))
    if [[ $lock_attempts -ge $max_lock_attempts ]]; then
      msg_error "Timeout while waiting for template lock after ${max_lock_attempts} attempts."
      msg_custom "💡" "${YW}" "Another container creation may be stuck. Check running processes or remove: $lockfile"
      exit 211
    fi
    msg_custom "⏳" "${YW}" "Another container is being created with this template. Waiting... (attempt ${lock_attempts}/${max_lock_attempts})"
  done

  LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"

  # Helper: append pct_create log to BUILD_LOG before exit so combined log has full context
  _flush_pct_log() {
    if [[ -s "${LOGFILE:-}" && -n "${BUILD_LOG:-}" ]]; then
      {
        echo ""
        echo "--- pct create output (${LOGFILE}) ---"
        cat "$LOGFILE"
        echo "--- end pct create output ---"
      } >>"$BUILD_LOG" 2>/dev/null || true
    fi
  }

  # Validate template before pct create (while holding lock)
  if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH" 2>/dev/null || echo 0)" -lt 1000000 ]]; then
    msg_info "Template file missing or too small – downloading"
    rm -f "$TEMPLATE_PATH"
    pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
      msg_error "Failed to download template '$TEMPLATE' to storage '$TEMPLATE_STORAGE'"
      exit 222
    }
    msg_ok "Template downloaded"
  elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
    if [[ -n "$ONLINE_TEMPLATE" ]]; then
      msg_info "Template appears corrupted – re-downloading"
      rm -f "$TEMPLATE_PATH"
      pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
        msg_error "Failed to re-download template '$TEMPLATE'"
        exit 222
      }
      msg_ok "Template re-downloaded"
    else
      msg_warn "Template appears corrupted, but no online version exists. Skipping re-download."
    fi
  fi

  # Release lock after template validation - pct create has its own internal locking
  exec 9>&-

  msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS"
  msg_debug "Logfile: $LOGFILE"

  # First attempt (PCT_OPTIONS is a multi-line string, use it directly)
  if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
    msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."

    # Check if CTID collision (race condition: ID claimed between validation and creation)
    if grep -qiE 'already exists|already in use' "$LOGFILE"; then
      local old_ctid="$CTID"
      CTID=$(get_valid_container_id "$((CTID + 1))")
      export CTID
      msg_warn "Container ID $old_ctid was claimed by another process. Retrying with ID $CTID"
      LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log"
      if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
        msg_ok "Container successfully created with new ID $CTID"
      else
        msg_error "Container creation failed even with new ID $CTID. See $LOGFILE"
        _flush_pct_log
        exit 209
      fi
    else
      # Not a CTID collision - check if template issue and retry with fresh download
      if grep -qiE 'unable to open|corrupt|invalid' "$LOGFILE"; then
        msg_info "Template may be corrupted – re-downloading"
        rm -f "$TEMPLATE_PATH"
        pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1
        msg_ok "Template re-downloaded"
      fi

      # Retry after repair
      if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
        # Fallback to local storage if not already on local
        if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
          msg_info "Retrying container creation with fallback to local storage"
          LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE"
          if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then
            msg_ok "Trying local storage fallback"
            msg_info "Downloading template to local"
            pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1
            msg_ok "Template downloaded to local"
          else
            msg_ok "Trying local storage fallback"
          fi
          if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
            # Local fallback also failed - check for LXC stack version issue
            if grep -qiE 'unsupported .* version' "$LOGFILE"; then
              msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
              offer_lxc_stack_upgrade_and_maybe_retry "yes"
              rc=$?
              case $rc in
              0) : ;; # success - container created, continue
              2)
                msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve"
                _flush_pct_log
                exit 231
                ;;
              3)
                msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE"
                _flush_pct_log
                exit 231
                ;;
              esac
            else
              msg_error "Container creation failed. See $LOGFILE"
              if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
                set -x
                pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
                set +x
              fi
              _flush_pct_log
              exit 209
            fi
          else
            msg_ok "Container successfully created using local fallback."
          fi
        else
          # Already on local storage and still failed - check LXC stack version
          if grep -qiE 'unsupported .* version' "$LOGFILE"; then
            msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
            offer_lxc_stack_upgrade_and_maybe_retry "yes"
            rc=$?
            case $rc in
            0) : ;; # success - container created, continue
            2)
              msg_error "Upgrade declined. Please update and re-run: apt update && apt install --only-upgrade pve-container lxc-pve"
              _flush_pct_log
              exit 231
              ;;
            3)
              msg_error "Upgrade and/or retry failed. Please inspect: $LOGFILE"
              _flush_pct_log
              exit 231
              ;;
            esac
          else
            msg_error "Container creation failed. See $LOGFILE"
            if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
              set -x
              pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
              set +x
            fi
            _flush_pct_log
            exit 209
          fi
        fi
      else
        msg_ok "Container successfully created after template repair."
      fi
    fi # close CTID collision else-branch
  fi

  # Verify container exists (allow up to 10s for pmxcfs sync in clusters)
  local _pct_visible=false
  for _pct_check in {1..10}; do
    if pct list | awk '{print $1}' | grep -qx "$CTID"; then
      _pct_visible=true
      break
    fi
    sleep 1
  done
  if [[ "$_pct_visible" != true ]]; then
    msg_error "Container ID $CTID not listed in 'pct list' after 10s. See $LOGFILE"
    msg_custom "🔧" "${YW}" "This can happen in clusters with pmxcfs sync delays."
    _flush_pct_log
    exit 215
  fi

  # Verify config rootfs
  grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf" || {
    msg_error "RootFS entry missing in container config. See $LOGFILE"
    _flush_pct_log
    exit 216
  }

  msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."

  # Append pct create log to BUILD_LOG for combined log visibility
  if [[ -s "$LOGFILE" && -n "${BUILD_LOG:-}" ]]; then
    {
      echo ""
      echo "--- pct create output ---"
      cat "$LOGFILE"
      echo "--- end pct create output ---"
    } >>"$BUILD_LOG" 2>/dev/null || true
  fi
}

# ==============================================================================
# SECTION 9: POST-INSTALLATION & FINALIZATION
# ==============================================================================

# ------------------------------------------------------------------------------
# description()
#
# - Sets container description with formatted HTML content
# - Includes:
#   * Community-Scripts logo
#   * Application name
#   * Links to GitHub, Discussions, Issues
#   * Ko-fi donation badge
# - Restarts ping-instances.service if present (monitoring)
# - Posts final "done" status to API telemetry
# ------------------------------------------------------------------------------
description() {
  IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
  local script_slug script_url donate_url

  script_slug="${SCRIPT_SLUG:-${NSAPP}}"
  script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')"
  script_url="https://community-scripts.org/scripts/${script_slug}"
  donate_url="https://community-scripts.org/donate"

  # Generate LXC Description
  DESCRIPTION=$(
    cat <<EOF
<div align='center'>
  <a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
    <img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
  </a>

  <h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>

  <p style='margin: 16px 0;'>
    <a href='${donate_url}' target='_blank' rel='noopener noreferrer'>
      <img src='https://img.shields.io/badge/❤️-Sponsoring%20%26%20Donations-FF5E5B' alt='Sponsoring and donations' />
    </a>
  </p>

  <p style='margin: 12px 0;'>
    <a href='${script_url}' target='_blank' rel='noopener noreferrer'>
      <img src='https://img.shields.io/badge/📦-Open%20Script%20Page-00617f' alt='Open script page' />
    </a>
  </p>

  <span style='margin: 0 10px;'>
    <i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
    <a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
  </span>
  <span style='margin: 0 10px;'>
    <i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
    <a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
  </span>
  <span style='margin: 0 10px;'>
    <i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
    <a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
  </span>
</div>
EOF
  )
  pct set "$CTID" -description "$DESCRIPTION"

  if [[ -f /etc/systemd/system/ping-instances.service ]]; then
    systemctl start ping-instances.service
  fi

  INSTALL_COMPLETE=true
  post_update_to_api "done" "none"
}

# ==============================================================================
# SECTION 10: ERROR HANDLING & EXIT TRAPS
# ==============================================================================

# ------------------------------------------------------------------------------
# ensure_log_on_host()
#
# - Ensures INSTALL_LOG points to a readable file on the host
# - If INSTALL_LOG points to a container path (e.g. /root/.install-*),
#   tries to pull it from the container and create a combined log
# - This allows get_error_text() to find actual error output for telemetry
# - Uses timeout on pct pull to prevent hangs on dead/unresponsive containers
# ------------------------------------------------------------------------------
ensure_log_on_host() {
  # Already readable on host? Nothing to do.
  [[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]] && return 0

  # Try pulling from container and creating combined log
  if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]] && command -v pct &>/dev/null; then
    local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
    if [[ ! -s "$combined_log" ]]; then
      # Create combined log
      {
        echo "================================================================================"
        echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
        echo "Container ID: ${CTID}"
        echo "Session ID: ${SESSION_ID}"
        echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
        echo "================================================================================"
        echo ""
      } >"$combined_log" 2>/dev/null || return 0
      # Append BUILD_LOG if it exists
      if [[ -f "${BUILD_LOG:-}" ]]; then
        {
          echo "================================================================================"
          echo "PHASE 1: CONTAINER CREATION (Host)"
          echo "================================================================================"
          cat "${BUILD_LOG}"
          echo ""
        } >>"$combined_log"
      fi
      # Pull INSTALL_LOG from container (with timeout to prevent hangs on dead containers)
      local temp_log="/tmp/.install-temp-${SESSION_ID}.log"
      if timeout 8 pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then
        {
          echo "================================================================================"
          echo "PHASE 2: APPLICATION INSTALLATION (Container)"
          echo "================================================================================"
          cat "$temp_log"
          echo ""
        } >>"$combined_log"
        rm -f "$temp_log"
      fi
    fi
    if [[ -s "$combined_log" ]]; then
      INSTALL_LOG="$combined_log"
    fi
  fi
}

# ==============================================================================
# TRAP MANAGEMENT
# ==============================================================================
# All traps (ERR, EXIT, INT, TERM, HUP) are set by catch_errors() in
# error_handler.func — called at the top of this file after sourcing.
#
# Do NOT set duplicate traps here. The handlers in error_handler.func
# (on_exit, on_interrupt, on_terminate, on_hangup, error_handler) already:
#   - Send telemetry via post_update_to_api / _send_abort_telemetry
#   - Stop orphaned containers via _stop_container_if_installing
#   - Collect logs via ensure_log_on_host
#   - Clean up lock files and spinner processes
#
# Previously, inline traps here overwrote catch_errors() traps, causing:
#   - error_handler() never fired (no error output, no cleanup dialog)
#   - on_hangup() never fired (SSH disconnect → stuck records)
#   - Duplicated logic in two places (hard to debug)
# ==============================================================================
