#!/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}}"

	# ARM64 Template default variables
	DEBIAN_DEFAULT_CODENAME="trixie"
	UBUNTU_DEFAULT_CODENAME="noble"
	ALPINE_DEFAULT_VERSION="3.23"

	# 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 "$(dirname "${BASH_SOURCE[0]}")/api.func"

if command -v curl >/dev/null 2>&1; then
	source "$(dirname "${BASH_SOURCE[0]}")/core.func"
	source "$(dirname "${BASH_SOURCE[0]}")/error_handler.func"
	load_functions
	catch_errors
elif command -v wget >/dev/null 2>&1; then
	source "$(dirname "${BASH_SOURCE[0]}")/core.func"
	source "$(dirname "${BASH_SOURCE[0]}")/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_hostname="$(hostname)"
		local current_ip="$(hostname -I | awk '{print $1}')"

		# Escape sed special chars in replacement strings (& \ |)
		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 "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 dev "$bridge" &>/dev/null; then
		return 1
	fi

	return 0
}

# ------------------------------------------------------------------------------
# validate_sdn_vnet()
#
# - Validates that an SDN vnet exists in the cluster config
# ------------------------------------------------------------------------------
validate_sdn_vnet() {
	local vnet="$1"
	[[ -z "$vnet" ]] && return 1
	[[ -f /etc/pve/sdn/vnets.cfg ]] && grep -qE "^vnet:[[:space:]]*${vnet}([[:space:]]|$)" /etc/pve/sdn/vnets.cfg && return 0
	command -v pvesh &>/dev/null && pvesh get "/cluster/sdn/vnets/${vnet}" &>/dev/null && return 0
	return 1
}

# ------------------------------------------------------------------------------
# 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"}
	SDN_VNET=${var_sdn_vnet:-""}
	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"}
	GATE=${var_gateway:-""}

	# Guard against invalid gateway combinations from defaults/app vars:
	# - DHCP must not force a static gateway
	# - Static IPv4 must use a gateway in the same subnet
	if [[ "$NET" == "dhcp" && -n "$GATE" ]]; then
		msg_warn "Ignoring var_gateway '$GATE' because var_net is 'dhcp'"
		GATE=""
	elif [[ "$NET" != "dhcp" && -n "$GATE" ]]; then
		if ! validate_gateway_in_subnet "$NET" "$GATE"; then
			msg_warn "Ignoring var_gateway '$GATE' because it is not in subnet of var_net '$NET'"
			GATE=""
		fi
	fi

	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

	HTTP_PROXY="${var_http_proxy:-}"
	HTTP_NO_PROXY="${var_http_no_proxy:-}"

	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:-""}
	# Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates
	if [[ "${var_tags:-}" == *community-script* ]]; then
		TAGS="${var_tags:-community-script}"
	else
		TAGS="community-script${var_tags:+;${var_tags}}"
	fi
	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_http_no_proxy var_http_proxy 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
		var_post_install var_sdn_vnet
	)

	# 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)
					# Normalize: strip spaces, trailing commas
					var_val="${var_val// /}"
					var_val="${var_val%%,}"
					var_val="${var_val##,}"
					if [[ -n "$var_val" ]] && [[ ! "$var_val" =~ ^[a-zA-Z0-9]+(,[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_sdn_vnet)
					if [[ -n "$var_val" ]] && ! validate_sdn_vnet "$var_val"; then
						msg_warn "SDN vnet '$var_val' from $file not found, ignoring"
						continue
					fi
					;;
				var_http_proxy)
					if [[ -n "$var_val" ]] && ! [[ "$var_val" =~ ^https?://[^[:space:]]+(:[0-9]+)?/?$ ]]; then
						msg_warn "Invalid HTTP proxy URL '$var_val' in $file, ignoring"
						continue
					fi
					;;
				var_http_no_proxy)
					if [[ -n "$var_val" ]] && [[ ! "$var_val" =~ ^[a-zA-Z0-9.*_:/-]+(,[a-zA-Z0-9.*_:/-]+)*$ ]]; then
						msg_warn "Invalid no_proxy value '$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_http_no_proxy var_http_proxy 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_post_install var_sdn_vnet
	)

	# 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

# HTTP/HTTPS proxy (optional - for networks requiring a proxy)
# var_http_proxy=http://proxy.local:8080
# var_http_no_proxy=localhost,127.0.0.1,.local

# 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

# Optional post-install script (host-side path to a *.sh on the Proxmox host)
# Runs ON THE HOST after the container is fully provisioned.
# Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG
# var_post_install=/opt/post-install/myhook.sh
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_http_no_proxy var_http_proxy 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
		var_post_install var_sdn_vnet
	)
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:-}"
	_http_proxy="${HTTP_PROXY:-${var_http_proxy:-}}"
	_http_no_proxy="${HTTP_NO_PROXY:-${var_http_no_proxy:-}}"
	_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 "$_http_proxy" ] && echo "var_http_proxy=$(_sanitize_value "$_http_proxy")"
		[ -n "$_http_no_proxy" ] && echo "var_http_no_proxy=$(_sanitize_value "$_http_no_proxy")"

		[ -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 "${var_sdn_vnet:-}" ] && echo "var_sdn_vnet=$(_sanitize_value "${var_sdn_vnet}")"

		[ -n "$_tpl_storage" ] && echo "var_template_storage=$(_sanitize_value "$_tpl_storage")"
		[ -n "$_ct_storage" ] && echo "var_container_storage=$(_sanitize_value "$_ct_storage")"
		[ -n "${var_post_install:-}" ] && echo "var_post_install=$(_sanitize_value "${var_post_install}")"
	} >"$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
	# Build TAGS: ensure community-script prefix, use semicolons (pct format), no duplicates
	if [[ "${var_tags:-}" == *community-script* ]]; then
		TAGS="${var_tags:-community-script}"
	else
		TAGS="community-script${var_tags:+;${var_tags}}"
	fi
	local STEP=1
	local MAX_STEP=30

	# 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 _sdn_vnet="${var_sdn_vnet:-}"
	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 _http_proxy="${var_http_proxy:-}"
	local _http_no_proxy="${var_http_no_proxy:-}"
	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}"
	local _post_install="${var_post_install:-}"

	# 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
		if [[ -f /etc/pve/sdn/vnets.cfg ]]; then
			while IFS= read -r vnet; do
				[[ -n "$vnet" ]] && BRIDGE_MENU_OPTIONS+=("sdn:${vnet}" "[SDN] ${vnet}")
			done < <(awk '/^vnet:/{print $2}' /etc/pve/sdn/vnets.cfg 2>/dev/null)
		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 [[ "$bridge_test" == sdn:* ]]; then
						local vnet_test="${bridge_test#sdn:}"
						if validate_sdn_vnet "$vnet_test"; then
							_sdn_vnet="$vnet_test"
							_bridge="${var_brg:-vmbr0}"
							((STEP++))
						else
							whiptail --msgbox "SDN vnet '$vnet_test' is not configured on this cluster." 8 58
						fi
					elif validate_bridge "$bridge_test"; then
						_bridge="$bridge_test"
						_sdn_vnet=""
						((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)
			# Keyctl is always required for unprivileged containers — skip dialog
			if [[ "$_ct_type" == "1" ]]; then
				_enable_keyctl="1"
				((STEP++))
				continue
			fi

			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\n(App default: ${var_keyctl:-0})" 14 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: HTTP/HTTPS Proxy
		# ═══════════════════════════════════════════════════════════════════════════
		24)
			local http_proxy_default_flag="--defaultno"
			[[ -n "$_http_proxy" ]] && http_proxy_default_flag=""

			if whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
				--title "HTTP/HTTPS PROXY" \
				--ok-button "Next" --cancel-button "Back" \
				$http_proxy_default_flag \
				--yesno "\nUse HTTP/HTTPS proxy?\n\nRequired when internet access must go through a proxy server.\n(e.g. corporate networks)\n\n(App default: ${var_http_proxy:-none})" 14 62; then
				if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
					--title "HTTP PROXY URL" \
					--inputbox "\nEnter HTTP proxy URL:\n(e.g. http://proxy.local:8080)" 12 62 "$_http_proxy" \
					3>&1 1>&2 2>&3); then
					_http_proxy="$result"
					local _no_proxy_default="${_http_no_proxy:-localhost,127.0.0.1,.local}"
					if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
						--title "NO_PROXY EXCEPTIONS" \
						--inputbox "\nEnter NO_PROXY exceptions (comma-separated):\nLeave empty for defaults." 12 62 "$_no_proxy_default" \
						3>&1 1>&2 2>&3); then
						_http_no_proxy="$result"
					fi
				fi
			else
				if [ $? -eq 1 ]; then
					_http_proxy=""
					_http_no_proxy=""
				else
					((STEP--))
					continue
				fi
			fi
			((STEP++))
			;;

		# ═══════════════════════════════════════════════════════════════════════════
		# STEP 25: Container Timezone
		# ═══════════════════════════════════════════════════════════════════════════
		25)
			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 26: Container Protection
		# ═══════════════════════════════════════════════════════════════════════════
		26)
			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 27: Device Node Creation (mknod)
		# ═══════════════════════════════════════════════════════════════════════════
		27)
			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 28: Mount Filesystems
		# ═══════════════════════════════════════════════════════════════════════════
		28)
			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
				# Normalize: strip spaces and trailing/leading commas
				result="${result// /}"
				result="${result%%,}"
				result="${result##,}"
				_mount_fs="$result"
				((STEP++))
			else
				((STEP--))
			fi
			;;

		# ═══════════════════════════════════════════════════════════════════════════
		# STEP 29: Optional host-side post-install hook (path on the Proxmox HOST)
		# ═══════════════════════════════════════════════════════════════════════════
		29)
			local _hook_prompt="Optional: absolute path to a *.sh file ON THE PROXMOX HOST.

It runs as root on the HOST (NOT in the LXC) after the container
is fully provisioned and started.

Available env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG.

Leave empty to skip."
			while true; do
				if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \
					--title "POST-INSTALL HOOK (HOST)" \
					--ok-button "Next" --cancel-button "Back" \
					--inputbox "$_hook_prompt" 16 70 "${_post_install}" \
					3>&1 1>&2 2>&3); then
					# Normalize: strip surrounding whitespace
					result="$(printf '%s' "$result" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
					if [[ -z "$result" ]]; then
						_post_install=""
						((STEP++))
						break
					fi
					# Reject obvious shell-meta sneaking through
					if [[ "$result" == *';'* || "$result" == *'$('* || "$result" == *'`'* || "$result" == *'&&'* || "$result" == *'||'* ]]; then
						whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \
							--msgbox "Path contains shell metacharacters. Please provide a plain absolute file path." 10 70
						continue
					fi
					if [[ "$result" != /* ]]; then
						whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID PATH" \
							--msgbox "Path must be absolute (start with /).\n\nGot: $result" 10 70
						continue
					fi
					if [[ ! -f "$result" ]]; then
						if ! whiptail --backtitle "Proxmox VE Helper Scripts" --title "FILE NOT FOUND" \
							--yesno "File does not exist on host:\n\n$result\n\nKeep this path anyway?" 12 70; then
							continue
						fi
					fi
					_post_install="$result"
					((STEP++))
					break
				else
					((STEP--))
					break
				fi
			done
			;;

		# ═══════════════════════════════════════════════════════════════════════════
		# STEP 30: Verbose Mode & Confirmation
		# ═══════════════════════════════════════════════════════════════════════════
		30)
			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 http_proxy_display="${_http_proxy:-(none)}"

			local post_install_display="${_post_install:-(none)}"
			local post_install_warn=""
			[[ -n "$_post_install" ]] && post_install_warn="
  ⚠ Hook runs as root on Proxmox HOST (not in LXC)"

			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
  HTTP Proxy: $http_proxy_display
  Verbose: $_verbose
  Post-Install Script: ${post_install_display}${post_install_warn}"

			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"
	HTTP_PROXY="$_http_proxy"
	HTTP_NO_PROXY="$_http_no_proxy"
	VERBOSE="$_verbose"
	var_post_install="$_post_install"

	# 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"
	var_sdn_vnet="$_sdn_vnet"
	var_http_proxy="$_http_proxy"
	var_http_no_proxy="$_http_no_proxy"

	# 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=""

	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}"
	[[ -n "${HTTP_PROXY:-}" ]] && echo -e "${INFO}${BOLD}${DGN}HTTP Proxy: ${BGN}$HTTP_PROXY${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 [[ "$(dpkg --print-architecture)" == "arm64" ]]; then
		echo -e "${INFO}${BOLD}${DGN}Architecture: ${BGN}arm64${CL}"
	fi
	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
	ensure_whiptail
	arch_check
	arm64_notice
	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"
		if is_unattended; then
			msg_error "Aborted: under-provisioned LXC in unattended mode (${current_cpu} CPU/${current_ram}MB RAM < ${var_cpu} CPU/${var_ram}MB RAM)"
			exit 113
		fi
		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)"
		if is_unattended; then
			msg_error "Aborted: storage too low in unattended mode (${usage}% used on /boot)"
			exit 114
		fi
		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
			[[ -d "$glob_path" ]] && glob_path="${glob_path%/}/*"
			local -a _scan_files
			mapfile -t _scan_files < <(compgen -G "$glob_path")
			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
}

# ------------------------------------------------------------------------------
# run_addon_updates()
#
# - Scans /usr/local/bin/update_* for addon update scripts installed alongside
#   the main application (e.g. by tools/addon/*.sh)
# - For each found addon, prompts the user (60s timeout, default no) whether
#   it should be updated as well
# - Skipped entirely when PHS_SILENT=1 to keep unattended updates predictable
# ------------------------------------------------------------------------------
run_addon_updates() {
	shopt -s nullglob
	local addons=(/usr/local/bin/update_*)
	shopt -u nullglob

	((${#addons[@]} == 0)) && return 0

	if [[ "${PHS_SILENT:-0}" == "1" ]]; then
		msg_info "Detected ${#addons[@]} addon update script(s) - skipping (PHS_SILENT)"
		return 0
	fi

	echo
	echo -e "${INFO}${YW} Detected installed addon update script(s):${CL}"
	local a name
	for a in "${addons[@]}"; do
		echo -e "${TAB}- ${a##*/update_}"
	done
	echo

	local ans
	for a in "${addons[@]}"; do
		name="${a##*/update_}"
		printf 'Do you also want to update addon "%s"? (y/N) [60s]: ' "$name"
		ans=""
		if read -r -t 60 ans; then :; else echo; fi
		case "${ans,,}" in
		y | yes)
			bash "$a" || msg_warn "Addon update for $name failed (rc=$?)"
			;;
		*)
			msg_info "Skipped addon: $name"
			;;
		esac
	done
}

runtime_script_status_guard() {
	local mode="${1:-}"
	local script_slug="${SCRIPT_SLUG:-${NSAPP:-}}"
	script_slug="$(echo "$script_slug" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')"
	[[ -z "$script_slug" ]] && return 0

	local api_url="https://db.community-scripts.org/api/collections/script_scripts/records?filter=(slug='${script_slug}')&perPage=1&fields=slug,is_disabled,is_deleted,disable_message,deleted_message,pinned_version,pin_reason"
	local response
	if ! response=$(curl -fsSL --connect-timeout 2 --max-time 3 "$api_url" 2>/dev/null); then
		msg_warn "Script status check is unavailable. Continuing without status verification."
		return 0
	fi

	local is_deleted is_disabled deleted_message disable_message pinned_version pin_reason info_url
	if printf '%s' "$response" | grep -qE '"items":[[:space:]]*\[[[:space:]]*\]'; then
		return 0
	fi

	# PocketBase returns a flat, fixed-field JSON blob; sed is enough here (no jq needed).
	is_deleted=$(printf '%s' "$response" | sed -n 's/.*"is_deleted"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' | head -1)
	is_disabled=$(printf '%s' "$response" | sed -n 's/.*"is_disabled"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' | head -1)
	deleted_message=$(printf '%s' "$response" | sed -n 's/.*"deleted_message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
	disable_message=$(printf '%s' "$response" | sed -n 's/.*"disable_message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
	pinned_version=$(printf '%s' "$response" | sed -n 's/.*"pinned_version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
	pin_reason=$(printf '%s' "$response" | sed -n 's/.*"pin_reason"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
	is_deleted=${is_deleted:-false}
	is_disabled=${is_disabled:-false}
	info_url="https://community-scripts.org/scripts/${script_slug}"

	if [[ "$is_deleted" == "true" ]]; then
		msg_error "This script is no longer available in community-scripts."
		[[ -n "$deleted_message" ]] && msg_error "$deleted_message"
		[[ -z "$deleted_message" ]] && msg_error "This script was removed and cannot be installed or updated."
		msg_warn "More info: ${info_url}"
		return 1
	fi

	if [[ "$is_disabled" == "true" ]]; then
		# Allow bypass via var_ignore_disable=true (still warn, but continue)
		case "${var_ignore_disable:-}" in
		1 | yes | true | on)
			msg_warn "This script is currently disabled in community-scripts."
			[[ -n "$disable_message" ]] && msg_warn "$disable_message"
			msg_warn "Bypassing disable status via var_ignore_disable — continuing at your own risk."
			msg_warn "More info: ${info_url}"
			return 0
			;;
		esac

		msg_error "This script is currently disabled in community-scripts."
		[[ -n "$disable_message" ]] && msg_error "$disable_message"
		[[ -z "$disable_message" ]] && msg_error "Updates and installs are temporarily disabled for this script."
		msg_warn "More info: ${info_url}"
		return 1
	fi

	if [[ "$mode" == "update" && -n "$pinned_version" && -n "${NSAPP:-}" ]]; then
		local current_file="$HOME/.${NSAPP}"
		if [[ -f "$current_file" ]]; then
			local installed pinned_clean installed_clean
			installed="$(<"$current_file")"
			pinned_clean="$pinned_version"
			installed_clean="$installed"
			[[ "$pinned_clean" =~ ^v[0-9] ]] && pinned_clean="${pinned_clean:1}"
			[[ "$installed_clean" =~ ^v[0-9] ]] && installed_clean="${installed_clean:1}"
			if [[ "$installed_clean" == "$pinned_clean" ]]; then
				if [[ -n "$pin_reason" ]]; then
					msg_info "You are already on the pinned version (${pinned_version}). ${pin_reason}"
				else
					msg_info "You are already on the pinned version (${pinned_version}). No newer update is offered intentionally — please do not open an issue unless you see an actual error."
				fi
				msg_info "More info: ${info_url}"
			fi
		fi
	fi

	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
		runtime_script_status_guard install || return 0
		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
		runtime_script_status_guard update || return 0
		update_script
		run_addon_updates
		update_motd_ip
		cleanup_lxc
	elif ! command -v whiptail &>/dev/null || ! [ -t 0 ] || [[ "$TERM" == "dumb" ]]; then
		msg_info "No interactive terminal detected – defaulting to silent update mode"
		VERBOSE="no"
		set_std_mode
		ensure_profile_loaded
		get_lxc_ip
		runtime_script_status_guard update || return 0
		update_script
		run_addon_updates
		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
		runtime_script_status_guard update || return 0
		update_script
		run_addon_updates
		update_motd_ip
		cleanup_lxc
	fi
}

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

# ------------------------------------------------------------------------------
# _apply_http_proxy_in_container()
#
# - Writes persistent proxy settings into the LXC before base package install
# - Ensures apt/apk/curl work behind corporate proxies during early setup
# ------------------------------------------------------------------------------
_apply_http_proxy_in_container() {
	local proxy="${HTTP_PROXY:-}"
	local noproxy="${HTTP_NO_PROXY:-}"
	[[ -z "$proxy" || -z "${CTID:-}" ]] && return 0
	[[ -z "$noproxy" ]] && noproxy="localhost,127.0.0.1,.local"

	msg_info "Applying HTTP proxy in container"
	local tmpf
	tmpf="$(mktemp)"
	cat >"$tmpf" <<EOF
export http_proxy="$proxy"
export https_proxy="$proxy"
export HTTP_PROXY="$proxy"
export HTTPS_PROXY="$proxy"
export no_proxy="$noproxy"
export NO_PROXY="$noproxy"
EOF
	pct push "$CTID" "$tmpf" /etc/profile.d/90-http-proxy.sh >/dev/null 2>&1 || {
		rm -f "$tmpf"
		msg_warn "Failed to push HTTP proxy profile into container"
		return 0
	}
	pct exec "$CTID" -- chmod 644 /etc/profile.d/90-http-proxy.sh >/dev/null 2>&1 || true

	if [[ "$var_os" != "alpine" && "${APT_CACHER:-}" != "yes" ]]; then
		cat >"$tmpf" <<EOF
Acquire::http::Proxy "$proxy";
Acquire::https::Proxy "$proxy";
EOF
		pct push "$CTID" "$tmpf" /etc/apt/apt.conf.d/95http-proxy >/dev/null 2>&1 || true
	fi
	rm -f "$tmpf"
	msg_ok "Applied HTTP proxy in container"
}

# ------------------------------------------------------------------------------
# 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}"
	if [[ -n "${var_sdn_vnet:-${SDN_VNET:-}}" ]]; then
		NET_STRING="-net0 name=eth0,bridge=${var_sdn_vnet:-$SDN_VNET}"
	fi

	# 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
		# Sanitize: strip spaces, trailing/leading commas, then convert commas to semicolons
		local _mount_clean="${ALLOW_MOUNT_FS// /}"
		_mount_clean="${_mount_clean%%,}"
		_mount_clean="${_mount_clean##,}"
		_mount_clean="${_mount_clean%%;}"
		_mount_clean="${_mount_clean//,/;}"
		if [ -n "$_mount_clean" ]; then
			[ -n "$FEATURES" ] && FEATURES="$FEATURES,"
			FEATURES="${FEATURES}mount=${_mount_clean}"
		fi
	fi

	# Build PCT_OPTIONS as string for export
	local _func_file
	local SCRIPT_DIR
	SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
	if [ "$var_os" == "alpine" ]; then
		_func_file="${SCRIPT_DIR}/alpine-install.func"
	else
		_func_file="${SCRIPT_DIR}/install.func"
	fi
	if [[ -f "$_func_file" ]]; then
		# Concatenate core dependencies + install func so the container has
		# everything via a single `source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"`.
		# install.func has `source $(dirname BASH_SOURCE)/core.func` which
		# resolves to /dev/core.func when sourced from stdin — this avoids that.
		local _core_file="${SCRIPT_DIR}/core.func"
		local _err_file="${SCRIPT_DIR}/error-handler.func"
		local _combined=""
		[[ -f "$_core_file" ]] && _combined+="$(cat "$_core_file")"$'\n'
		[[ -f "$_err_file" ]] && _combined+="$(cat "$_err_file")"$'\n'
		_combined+="$(cat "$_func_file")"
		export FUNCTIONS_FILE_PATH="$_combined"
	else
		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")"
	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"
	if [[ -n "${HTTP_PROXY:-}" ]]; then
		export HTTP_PROXY HTTPS_PROXY="$HTTP_PROXY" http_proxy="$HTTP_PROXY" https_proxy="$HTTP_PROXY"
		export NO_PROXY="${HTTP_NO_PROXY:-}" no_proxy="${HTTP_NO_PROXY:-}"
		export var_http_proxy="$HTTP_PROXY"
		export var_http_no_proxy="${HTTP_NO_PROXY:-}"
	fi
	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_ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m)"
	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"
		check_storage_health "$CONTAINER_STORAGE" "$DISK_SIZE"
	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

		# Map each DRI render/card node to its owning GPU via the PCI vendor ID
		# exposed in sysfs (/sys/class/drm/<node>/device/vendor). This is required
		# on multi-GPU hosts: globbing every /dev/dri node onto the first detected
		# vendor would pass through *both* GPUs and skip the selection prompt.
		if [[ -d /dev/dri ]]; then
			for d in /dev/dri/renderD* /dev/dri/card*; do
				[[ -e "$d" ]] || continue
				local node vendor
				node=$(basename "$d")
				vendor=$(cat "/sys/class/drm/${node}/device/vendor" 2>/dev/null || true)
				case "$vendor" in
				0x8086) INTEL_DEVICES+=("$d") ;;        # Intel
				0x1002 | 0x1022) AMD_DEVICES+=("$d") ;; # AMD/ATI
				0x10de) ;;                              # NVIDIA - handled via /dev/nvidia* below
				*) msg_debug "Unmapped GPU vendor '${vendor:-unknown}' for $d" ;;
				esac
			done
		fi

		[[ ${#INTEL_DEVICES[@]} -gt 0 ]] && msg_custom "🎮" "${BL}" "Detected Intel GPU"
		[[ ${#AMD_DEVICES[@]} -gt 0 ]] && msg_custom "🎮" "${RD}" "Detected AMD GPU"

		# AMD compute (ROCm) additionally needs /dev/kfd alongside the render nodes
		if [[ ${#AMD_DEVICES[@]} -gt 0 && -e /dev/kfd ]]; then
			AMD_DEVICES+=("/dev/kfd")
		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

	_apply_http_proxy_in_container

	# 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 'set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; 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 '
        set -a
        [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh
        set +a
        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

		local _base_pkgs="sudo curl mc gnupg2 jq"
		if [[ "${ARCH:-amd64}" == "arm64" ]]; then
			_base_pkgs+=" openssh-server wget gcc"
		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 "set -a; [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh; set +a; apt-get update 2>&1 && apt-get install -y ${_base_pkgs} 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})."
			msg_custom "ℹ️" "${YW}" "Probing alternate mirrors (this can take 1-2 minutes on network issues)"
			local mirror_exit=0
			pct exec "$CTID" -- env APT_BASE="$_base_pkgs" bash -c '
        set -a
        [ -f /etc/profile.d/90-http-proxy.sh ] && . /etc/profile.d/90-http-proxy.sh
        set +a
        DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian")
        echo "  Mirror fallback for distro: $DISTRO"

        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)
        echo "  Phase 1/3: Scanning global mirrors for reachability..."
        OTHERS_OK=$(scan_reachable "$OTHERS")
        OTHERS_PICK=$(printf "%s\n" $OTHERS_OK | shuf | head -3 | xargs)

        if [ -z "$OTHERS_PICK" ]; then
          echo "  No reachable global mirrors found"
        fi

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

        # Phase 2: Try primary mirror
        echo "  Phase 2/3: Trying 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
        echo "  Phase 3/3: Scanning regional mirrors..."
        REGIONAL_OK=$(scan_reachable "$REGIONAL")
        REGIONAL_PICK=$(printf "%s\n" $REGIONAL_OK | shuf | head -3 | xargs)

        if [ -z "$REGIONAL_PICK" ]; then
          echo "  No reachable regional mirrors found"
        fi

        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 ${_base_pkgs} >/dev/null 2>&1
          " && break
					msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
				done
				if [[ "$custom_mirror" == "skip" ]]; then
					msg_warn "Mirror selection aborted by user ('skip')"
					msg_warn "Aborting installation before base package bootstrap"
					post_update_to_api "aborted" "130" "force"
					install_exit_code=130
				fi
			elif [[ $mirror_exit -ne 0 ]]; then
				msg_error "apt-get base packages installation failed"
				install_exit_code=100
			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

		# Use local install script if available, fallback to remote download
		local _local_install="${SCRIPT_DIR}/../install/${var_install}.sh"
		if [[ -f "$_local_install" ]]; then
			lxc-attach -n "$CTID" -- bash -c "$(cat "$_local_install")"
		else
			lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
		fi
		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 /tmp/scripts/core/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
					if [[ -f "$_local_install" ]]; then
						lxc-attach -n "$CTID" -- bash -c "$(cat "$_local_install")"
					else
						lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
					fi
					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"
	sed -i -E "s|(dev[0-9]+: /dev/kfd),gid=[0-9]+|\1,gid=${render_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
      if [ -e /dev/kfd ]; then
        chgrp ${render_gid} /dev/kfd 2>/dev/null || true
        chmod 660 /dev/kfd 2>/dev/null || true
      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"
			check_storage_health "$STORAGE_RESULT" "$DISK_SIZE"
			# 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
}

# ------------------------------------------------------------------------------
# check_storage_health()
#
# - Warns or blocks when target storage is nearly full
# - Complements validate_storage_space (free space vs. requested disk)
# - Uses pvesm status usage when available
# ------------------------------------------------------------------------------
check_storage_health() {
	local storage="$1"
	local required_gb="${2:-8}"
	[[ -z "$storage" ]] && return 0

	local storage_line storage_type total_kb avail_kb used_pct free_gb_fmt
	storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}')
	[[ -z "$storage_line" ]] && return 0

	storage_type=$(awk '{print $2}' <<<"$storage_line")
	total_kb=$(awk '{print $4}' <<<"$storage_line")
	avail_kb=$(awk '{print $6}' <<<"$storage_line")

	[[ -z "$total_kb" || "$total_kb" == "0" || -z "$avail_kb" ]] && return 0

	used_pct=$(((total_kb - avail_kb) * 100 / total_kb))
	free_gb_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$avail_kb" 2>/dev/null || echo "${avail_kb}KB")

	if ((used_pct >= 95)); then
		msg_warn "Storage '${storage}' (${storage_type}) is ${used_pct}% full (${free_gb_fmt} free)"
		if ! is_unattended && command -v whiptail >/dev/null 2>&1 && [[ -t 0 ]]; then
			if ! whiptail --backtitle "Proxmox VE Helper Scripts" \
				--title "STORAGE ALMOST FULL" \
				--yesno "Storage '${storage}' is ${used_pct}% full.\n\nType: ${storage_type}\nAvailable: ${free_gb_fmt}\nRequired for CT: ${required_gb} GB\n\nContinue anyway?" 14 70; then
				msg_error "Installation cancelled – storage nearly full"
				exit 214
			fi
		elif ((used_pct >= 98)); then
			msg_error "Storage '${storage}' is ${used_pct}% full – refusing install in unattended mode"
			exit 214
		fi
	elif ((used_pct >= 85)); then
		msg_warn "Storage '${storage}' (${storage_type}) is ${used_pct}% full (${free_gb_fmt} free)"
	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"; }

	# Switch to the previous OS major version template and retry pct create
	# Determines the fallback version automatically based on available templates.
	# Returns: 0 = success, 1 = failed
	fallback_to_previous_os_version() {
		local old_template="$TEMPLATE"
		local os_type="${PCT_OSTYPE:-}"
		local current_ver="${PCT_OSVERSION:-}"

		# Determine template search pattern based on OS type
		local tpl_pattern=""
		case "$os_type" in
		debian | ubuntu) tpl_pattern="-standard_" ;;
		alpine | fedora | rocky | centos) tpl_pattern="-default_" ;;
		*) tpl_pattern="" ;;
		esac

		msg_info "Searching for an older $os_type template (current: $os_type $current_ver)"

		# Collect all available versions for this OS type (local + online)
		local -a all_versions=()

		# Local templates
		mapfile -t _local_vers < <(
			pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
				awk -v os="$os_type" -v pat="$tpl_pattern" '$1 ~ ("^"os"|/"os) && $1 ~ pat {print $1}' |
				sed 's|.*/||' |
				sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
				sort -u -V
		)
		all_versions+=("${_local_vers[@]}")

		# Online templates (only if needed)
		if command -v timeout &>/dev/null; then
			timeout 30 pveam update >/dev/null 2>&1 || true
		else
			pveam update >/dev/null 2>&1 || true
		fi
		mapfile -t _online_vers < <(
			pveam available -section system 2>/dev/null |
				awk '{print $2}' |
				grep -E "^${os_type}-[0-9]" |
				{ [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } |
				sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
				sort -u -V 2>/dev/null || true
		)
		all_versions+=("${_online_vers[@]}")

		# Deduplicate and sort, find the highest version below current
		local fallback_ver=""
		fallback_ver=$(printf '%s\n' "${all_versions[@]}" | sort -u -V | awk -v cur="$current_ver" '{
      # Compare major versions: extract major part
      split($0, a, ".")
      split(cur, b, ".")
      if (a[1]+0 < b[1]+0) ver=$0
    } END { if (ver) print ver }')

		if [[ -z "$fallback_ver" ]]; then
			msg_error "No older $os_type template version found."
			return 1
		fi

		msg_ok "Fallback version: $os_type $fallback_ver"

		# Find the actual template file for this version
		local fallback_search="${os_type}-${fallback_ver}"
		local fallback_template=""

		# Check local first
		mapfile -t _fb_local < <(
			pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
				awk -v search="$fallback_search" -v pat="$tpl_pattern" '$1 ~ search && $1 ~ pat {print $1}' |
				sed 's|.*/||' | sort -t - -k 2 -V
		)
		if [[ ${#_fb_local[@]} -gt 0 ]]; then
			fallback_template="${_fb_local[-1]}"
		else
			# Check online
			mapfile -t _fb_online < <(
				pveam available -section system 2>/dev/null |
					awk '{print $2}' |
					grep -E "^${fallback_search}.*${tpl_pattern}" |
					sort -t - -k 2 -V 2>/dev/null || true
			)
			[[ ${#_fb_online[@]} -gt 0 ]] && fallback_template="${_fb_online[-1]}"
		fi

		if [[ -z "$fallback_template" ]]; then
			msg_error "No template found for $os_type $fallback_ver."
			return 1
		fi

		msg_ok "Found template: $fallback_template"

		# Download if needed
		local fallback_path
		fallback_path="$(pvesm path "$TEMPLATE_STORAGE:vztmpl/$fallback_template" 2>/dev/null || true)"
		[[ -z "$fallback_path" ]] && fallback_path="/var/lib/vz/template/cache/$fallback_template"

		if [[ ! -f "$fallback_path" ]]; then
			msg_info "Downloading $os_type $fallback_ver template"
			if ! pveam download "$TEMPLATE_STORAGE" "$fallback_template" >>"${BUILD_LOG:-/dev/null}" 2>&1; then
				msg_error "Failed to download $os_type $fallback_ver template."
				return 1
			fi
			msg_ok "Template downloaded"
		fi

		# Update variables
		TEMPLATE="$fallback_template"
		TEMPLATE_PATH="$fallback_path"
		PCT_OSVERSION="$fallback_ver"
		export PCT_OSVERSION

		# Retry pct create
		msg_info "Retrying container creation with $os_type $fallback_ver"
		if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
			msg_ok "Container created successfully with $os_type $fallback_ver (fallback from $old_template)."
			return 0
		else
			msg_error "Container creation with $os_type $fallback_ver also failed. See $LOGFILE"
			return 1
		fi
	}

	# Offer upgrade for pve-container/lxc-pve if candidate > installed; optional auto-retry pct create
	# Returns:
	#   0 = no upgrade needed / container created after upgrade or explicit fallback
	#   1 = upgraded (and if do_retry=yes and retry succeeded, creation done)
	#   2 = user chose ignore
	#   3 = upgrade attempted but failed OR retry failed
	#   4 = user cancelled
	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
		local _ans

		has_previous_os_version_template() {
			local os_type="${PCT_OSTYPE:-}"
			local current_ver="${PCT_OSVERSION:-}"
			local tpl_pattern="${TEMPLATE_PATTERN:-${TEMPLATE:-}}"
			local -a all_versions=()

			[[ -n "$os_type" && -n "$current_ver" ]] || return 1

			mapfile -t _local_vers < <(
				pveam list "$TEMPLATE_STORAGE" 2>/dev/null |
					awk '{print $1}' |
					sed 's|.*/||' |
					grep -E "^${os_type}-[0-9]" |
					{ [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } |
					sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
					sort -u -V
			)
			all_versions+=("${_local_vers[@]}")

			if command -v timeout &>/dev/null; then
				timeout 30 pveam update >/dev/null 2>&1 || true
			else
				pveam update >/dev/null 2>&1 || true
			fi
			mapfile -t _online_vers < <(
				pveam available -section system 2>/dev/null |
					awk '{print $2}' |
					grep -E "^${os_type}-[0-9]" |
					{ [[ -n "$tpl_pattern" ]] && grep "$tpl_pattern" || cat; } |
					sed -E "s/^${os_type}-([0-9]+(\.[0-9]+)?).*/\1/" |
					sort -u -V 2>/dev/null || true
			)
			all_versions+=("${_online_vers[@]}")

			printf '%s\n' "${all_versions[@]}" | sort -u -V | awk -v cur="$current_ver" '
        {
          split($0, a, ".")
          split(cur, b, ".")
          if (a[1]+0 < b[1]+0) found=1
        }
        END { exit found ? 0 : 1 }
      '
		}

		_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
		local _disable_update="${DISABLE_UPDATE:-no}"
		if [[ "${PHS_SILENT:-0}" == "1" || "${_disable_update,,}" == "yes" ]]; then
			msg_info "Skipping host LXC stack upgrade prompt (unattended mode)"
			return 2
		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 "  note        : option 1 runs host apt update + apt upgrade"
		echo

		# Offer older OS version fallback when template version might be too new for LXC stack
		local _has_fallback_option=false
		if [[ "$do_retry" == "yes" ]] && has_previous_os_version_template; then
			_has_fallback_option=true
			echo "  [1] Run host upgrade now (recommended). WARNING: this runs apt upgrade and updates all Packages on your host!"
			echo "  [2] Use an older ${PCT_OSTYPE} template instead (may not work with all scripts)"
			echo "  [3] Ignore"
			echo "  [4] Cancel"
			echo
			read -rp "Select option [1/2/3/4]: " _ans </dev/tty
		else
			echo "  [1] Run host upgrade now (recommended). WARNING: this runs apt upgrade and updates all Packages on your host!"
			echo "  [2] Ignore"
			echo "  [3] Cancel"
			echo
			read -rp "Select option [1/2/3]: " _ans </dev/tty
		fi

		if [[ "$_has_fallback_option" == true ]]; then
			case "$_ans" in
			1)
				_ans="y"
				;;
			2)
				if fallback_to_previous_os_version; then
					return 0
				else
					return 3
				fi
				;;
			3)
				return 2
				;;
			4)
				return 4
				;;
			*)
				return 4
				;;
			esac
		else
			case "$_ans" in
			1)
				_ans="y"
				;;
			2)
				return 2
				;;
			*)
				return 4
				;;
			esac
		fi

		case "${_ans,,}" in
		y | yes)
			msg_info "Running host upgrade for LXC stack compatibility"
			apt_update_safe
			if $STD apt-get upgrade -y; then
				msg_ok "LXC stack upgraded."
				# Verify pct binary still works after upgrade (partial upgrades can break Perl modules)
				if ! pct list &>/dev/null; then
					msg_error "LXC stack upgrade caused PVE tool breakage (likely Perl module incompatibility)."
					msg_custom "⚠️" "${YW}" "A partial package upgrade has left the PVE stack in an inconsistent state."
					msg_custom "🔧" "${YW}" "Please run the following on the Proxmox host, then retry:"
					echo -e "${TAB}  apt update && apt upgrade -y"
					echo -e "${TAB}  reboot"
					return 3
				fi
				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
	}

	ARCH="$(dpkg --print-architecture)"

	# Maps OS type + version to the release variant name used by ARM64 template sources.
	arm64_template_variant() {
		case "$1:$2" in
		debian:12) echo "bookworm" ;;
		debian:13) echo "trixie" ;;
		debian:) echo "$DEBIAN_DEFAULT_CODENAME" ;;

		ubuntu:24.04) echo "noble" ;;
		ubuntu:26.04) echo "questing" ;;
		ubuntu:) echo "$UBUNTU_DEFAULT_CODENAME" ;;

		alpine:*) echo "${2:-$ALPINE_DEFAULT_VERSION}" ;;

		*) return 1 ;;
		esac
	}

	# Downloads an ARM64 LXC rootfs template from jenkins.linuxcontainers.org.
	download_arm64_template() {
		local dest="$1" url

		mkdir -p "$(dirname "$dest")" || {
			msg_error "Cannot create template dir."
			exit 207
		}

		url="https://jenkins.linuxcontainers.org/job/image-${PCT_OSTYPE}/architecture=arm64,release=${CUSTOM_TEMPLATE_VARIANT},variant=default/lastStableBuild/artifact/rootfs.tar.xz"

		msg_info "Downloading ${PCT_OSTYPE^} ${CUSTOM_TEMPLATE_VARIANT} ARM64 template"
		local patched="${dest%/*}/.${dest##*/}.patched"
		(
			flock -x 200
			if [[ -f "$patched" ]] && [[ -s "$dest" ]] && xz -t "$dest" 2>/dev/null; then
				exit 0
			fi
			if ! curl -fsSL -o "$dest" "$url"; then
				msg_error "Failed to download ARM64 template from: $url"
				exit 208
			fi
			# Some linuxcontainers.org rootfs builds ship without /etc/network,
			# which makes pve-container's post-create hook fail when it writes the
			# interfaces file. This appends an empty /etc/network/interfaces if missing.
			if ! tar -tJf "$dest" 2>/dev/null | grep -q '/etc/network/$'; then
				fixdir=$(mktemp -d)
				tmptar="${dest%.xz}"
				mkdir -p "$fixdir/etc/network"
				: >"$fixdir/etc/network/interfaces"
				if xz -d -f "$dest" && tar -C "$fixdir" --append -f "$tmptar" ./etc/network && xz -1 -f "$tmptar"; then
					rm -rf "$fixdir"
				else
					rm -rf "$fixdir" "$tmptar"
					msg_error "Failed to patch ARM64 template (missing /etc/network)"
					exit 208
				fi
			fi
			touch "$patched"
		) 200>"${dest}.lock"
		local dl_rc=$?
		if [[ $dl_rc -ne 0 ]]; then
			exit "$dl_rc"
		fi
		msg_ok "Downloaded ARM64 LXC template"
	}

	download_template() {
		local dest="${1:-$TEMPLATE_PATH}"
		if [[ "$ARCH" == "arm64" ]]; then
			download_arm64_template "$dest"
		else
			pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1 || {
				msg_error "Failed to download template '$TEMPLATE' to storage '$TEMPLATE_STORAGE'"
				exit 222
			}
		fi
	}

	# ------------------------------------------------------------------------------
	# 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'"
	# Check if storage.cfg is accessible (pmxcfs must be mounted)
	if [[ ! -f /etc/pve/storage.cfg ]]; then
		if ! mountpoint -q /etc/pve 2>/dev/null; then
			msg_error "Proxmox cluster filesystem (pmxcfs) is not mounted at /etc/pve."
			msg_custom "🔧" "${YW}" "Try: systemctl restart pve-cluster"
		else
			msg_error "/etc/pve/storage.cfg does not exist."
			msg_custom "🔧" "${YW}" "Check Proxmox cluster filesystem integrity: pvecm status"
		fi
		exit 213
	fi

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

	# Fallback: use pvesm status to determine storage type
	if [[ -z "$STORAGE_TYPE" ]]; then
		STORAGE_TYPE=$(pvesm status -storage "$CONTAINER_STORAGE" 2>/dev/null | awk 'NR>1{print $2}')
	fi

	if [[ -z "$STORAGE_TYPE" ]]; then
		msg_error "Storage '$CONTAINER_STORAGE' not found in /etc/pve/storage.cfg"
		msg_custom "📋" "${YW}" "Available storages: $(pvesm status 2>/dev/null | awk 'NR>1{printf "%s (%s) ", $1, $2}' || echo 'n/a')"
		if [[ -r /etc/pve/storage.cfg ]]; then
			msg_custom "📋" "${YW}" "Storage definitions found in config:"
			grep -E '^[a-z]+:' /etc/pve/storage.cfg 2>/dev/null | while IFS= read -r _line; do
				echo "${TAB}  $_line"
			done
		fi
		msg_custom "📖" "${YW}" "See https://pve.proxmox.com/wiki/Storage for storage configuration details."
		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."
		msg_custom "💡" "${YW}" "Enable 'Disk image' (rootdir) for storage '${CONTAINER_STORAGE}' in:"
		msg_custom "   " "${YW}" "Datacenter → Storage → ${CONTAINER_STORAGE} → Edit → Content"
		msg_custom "📖" "${YW}" "See: https://pve.proxmox.com/wiki/Storage"
		msg_custom "🔗" "${YW}" "Help: https://github.com/community-scripts/ProxmoxVE/discussions"
		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
	# ------------------------------------------------------------------------------
	CUSTOM_TEMPLATE_VARIANT=""

	if [[ "$ARCH" == "arm64" ]]; then
		# ARM64: use custom template download from linuxcontainers.org / GitHub
		msg_info "Preparing ARM64 template"

		CUSTOM_TEMPLATE_VARIANT=$(arm64_template_variant "$PCT_OSTYPE" "${PCT_OSVERSION:-}") || {
			msg_error "No ARM64 template mapping for ${PCT_OSTYPE} ${PCT_OSVERSION:-latest}"
			exit 207
		}

		TEMPLATE="${PCT_OSTYPE}-${CUSTOM_TEMPLATE_VARIANT}-rootfs.tar.xz"
		TEMPLATE_SOURCE="custom-arm64"

		# Resolve template path
		TEMPLATE_PATH="$(pvesm path "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" 2>/dev/null || true)"
		if [[ -z "$TEMPLATE_PATH" ]]; then
			local _tpl_base
			_tpl_base=$(awk -v s="$TEMPLATE_STORAGE" '
        $0 ~ "^[^:]+:[[:space:]]*" s "$" {f=1; next}
        f && /^[^[:space:]]/ {f=0}
        f && $1 == "path" {print $2; exit}
      ' /etc/pve/storage.cfg)
			TEMPLATE_PATH="${_tpl_base:-/var/lib/vz}/template/cache/$TEMPLATE"
		fi

		# Download if missing, too small, or corrupt
		if [[ ! -f "$TEMPLATE_PATH" ]]; then
			download_arm64_template "$TEMPLATE_PATH"
		elif [[ "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]] || ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
			msg_warn "Local template invalid - re-downloading."
			rm -f "$TEMPLATE_PATH"
			download_arm64_template "$TEMPLATE_PATH"
		else
			msg_ok "Template ${BL}$TEMPLATE${CL} found locally."
		fi

	else
		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"
				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 press 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 it."
			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
	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"
		download_template
		msg_ok "Template downloaded"
	elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then
		if [[ "$ARCH" == "arm64" || -n "$ONLINE_TEMPLATE" ]]; then
			msg_info "Template appears corrupted - re-downloading"
			rm -f "$TEMPLATE_PATH"
			download_template
			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)
	# Disable globbing: unquoted $PCT_OPTIONS needs word-splitting but must not glob-expand
	# (e.g. passwords containing * or ? would match filenames otherwise)
	set -f
	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 for Perl module breakage (partial PVE upgrade)
		if grep -qiE 'Compilation failed|Bareword.*not allowed' "$LOGFILE"; then
			msg_error "Container creation failed due to broken Perl modules on the PVE host."
			msg_custom "⚠️" "${YW}" "This usually happens after a partial PVE package upgrade."
			msg_custom "🔧" "${YW}" "Please run the following on the Proxmox host, then retry:"
			echo -e "${TAB}  apt update && apt dist-upgrade -y"
			echo -e "${TAB}  reboot"
			_flush_pct_log
			exit 232
		fi

		# 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"
				download_template
				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"
						if [[ "$ARCH" == "arm64" ]]; then
							download_arm64_template "$LOCAL_TEMPLATE_PATH"
						else
							pveam download local "$TEMPLATE" >>"${BUILD_LOG:-/dev/null}" 2>&1
						fi
						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 "LXC stack upgrade ignored. Please inspect: $LOGFILE"
								_flush_pct_log
								exit 231
								;;
							3)
								msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE"
								_flush_pct_log
								exit 231
								;;
							4)
								msg_error "Cancelled by user."
								_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 "LXC stack upgrade ignored. Please inspect: $LOGFILE"
							_flush_pct_log
							exit 231
							;;
						3)
							msg_error "LXC stack upgrade failed. Please inspect: $LOGFILE"
							_flush_pct_log
							exit 231
							;;
						4)
							msg_error "Cancelled by user."
							_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
	set +f # re-enable globbing after pct create block

	# 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

	# Optional host-side post-install hook
	# Path comes from var_post_install (default.vars / app.vars / advanced settings).
	# Runs ON THE PROXMOX HOST after the container is up and configured.
	# Exposed env vars: APP, NSAPP, CTID, IP, HN, STORAGE, BRG.
	# Output (stdout/stderr) is captured to /var/log/community-scripts/post-install-<CTID>.log
	if [[ -n "${var_post_install:-}" ]]; then
		local _hook_log_dir="/var/log/community-scripts"
		local _hook_log="${_hook_log_dir}/post-install-${CTID}.log"
		mkdir -p "$_hook_log_dir" 2>/dev/null || true

		if [[ ! -f "${var_post_install}" ]]; then
			msg_error "Post-install hook not found on host: ${var_post_install}"
			whiptail --backtitle "Proxmox VE Helper Scripts" \
				--title "POST-INSTALL HOOK FAILED" \
				--msgbox "The configured post-install hook was not found on the Proxmox host:\n\n${var_post_install}\n\nThe LXC was created successfully, but the hook did NOT run." 14 72 || true
		else
			msg_info "Running post-install hook: ${var_post_install}"
			local _hook_rc=0
			APP="$APP" NSAPP="${NSAPP:-}" CTID="$CTID" IP="$IP" HN="${HN:-}" \
				STORAGE="${STORAGE:-}" BRG="${BRG:-}" \
				bash "${var_post_install}" >"${_hook_log}" 2>&1 || _hook_rc=$?
			if [[ $_hook_rc -eq 0 ]]; then
				msg_ok "Post-install hook completed (log: ${_hook_log})"
			else
				msg_error "Post-install hook failed (rc=${_hook_rc}) – see ${_hook_log}"
				local _hook_tail=""
				_hook_tail="$(tail -n 15 "${_hook_log}" 2>/dev/null || true)"
				whiptail --backtitle "Proxmox VE Helper Scripts" \
					--title "POST-INSTALL HOOK FAILED" \
					--msgbox "Hook exited with code ${_hook_rc}.\n\nScript: ${var_post_install}\nLog:    ${_hook_log}\n\n--- Last log lines ---\n${_hook_tail}\n\nThe LXC itself was created successfully." 22 78 || true
			fi
		fi
	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)
# ==============================================================================
