#!/bin/sh
# SecuBox HAProxy Controller
# Copyright (C) 2025 CyberMind.fr

# Source OpenWrt functions for UCI iteration
. /lib/functions.sh

CONFIG="haproxy"
LXC_NAME="haproxy"

# Paths
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
DATA_PATH="/srv/haproxy"
SHARE_PATH="/usr/share/haproxy"
CERTS_PATH="$DATA_PATH/certs"
CONFIG_PATH="$DATA_PATH/config"

# Logging
log_info() { echo "[INFO] $*"; logger -t haproxy "$*"; }
log_warn() { echo "[WARN] $*" >&2; logger -t haproxy -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t haproxy -p err "$*"; }
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }

# Helpers
require_root() {
	[ "$(id -u)" -eq 0 ] || { log_error "Root required"; exit 1; }
}

has_lxc() { command -v lxc-start >/dev/null 2>&1; }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }

# Firewall management - auto-open ports when publishing
firewall_ensure_haproxy_rules() {
	local http_port="${1:-80}"
	local https_port="${2:-443}"
	local changed=0

	# Check if HTTP rule exists
	local http_rule_exists=0
	local i=0
	while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
		local name=$(uci -q get firewall.@rule[$i].name)
		if [ "$name" = "HAProxy-HTTP" ]; then
			http_rule_exists=1
			break
		fi
		i=$((i + 1))
	done

	# Create HTTP rule if missing
	if [ "$http_rule_exists" = "0" ]; then
		log_info "Creating firewall rule for HTTP (port $http_port)..."
		uci add firewall rule
		uci set firewall.@rule[-1].name='HAProxy-HTTP'
		uci set firewall.@rule[-1].src='wan'
		uci set firewall.@rule[-1].dest_port="$http_port"
		uci set firewall.@rule[-1].proto='tcp'
		uci set firewall.@rule[-1].target='ACCEPT'
		uci set firewall.@rule[-1].enabled='1'
		changed=1
	fi

	# Check if HTTPS rule exists
	local https_rule_exists=0
	i=0
	while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
		local name=$(uci -q get firewall.@rule[$i].name)
		if [ "$name" = "HAProxy-HTTPS" ]; then
			https_rule_exists=1
			break
		fi
		i=$((i + 1))
	done

	# Create HTTPS rule if missing
	if [ "$https_rule_exists" = "0" ]; then
		log_info "Creating firewall rule for HTTPS (port $https_port)..."
		uci add firewall rule
		uci set firewall.@rule[-1].name='HAProxy-HTTPS'
		uci set firewall.@rule[-1].src='wan'
		uci set firewall.@rule[-1].dest_port="$https_port"
		uci set firewall.@rule[-1].proto='tcp'
		uci set firewall.@rule[-1].target='ACCEPT'
		uci set firewall.@rule[-1].enabled='1'
		changed=1
	fi

	# Apply changes
	if [ "$changed" = "1" ]; then
		uci commit firewall
		/etc/init.d/firewall reload 2>/dev/null || true
		log_info "Firewall rules updated - ports $http_port and $https_port open on WAN"
	fi
}

firewall_check_haproxy_rules() {
	local http_ok=0 https_ok=0
	local i=0
	while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
		local name=$(uci -q get firewall.@rule[$i].name)
		local enabled=$(uci -q get firewall.@rule[$i].enabled)
		[ "$name" = "HAProxy-HTTP" ] && [ "$enabled" != "0" ] && http_ok=1
		[ "$name" = "HAProxy-HTTPS" ] && [ "$enabled" != "0" ] && https_ok=1
		i=$((i + 1))
	done
	[ "$http_ok" = "1" ] && [ "$https_ok" = "1" ]
}

# Load configuration
load_config() {
	http_port="$(uci_get main.http_port)" || http_port="80"
	https_port="$(uci_get main.https_port)" || https_port="443"
	stats_port="$(uci_get main.stats_port)" || stats_port="8404"
	stats_enabled="$(uci_get main.stats_enabled)" || stats_enabled="1"
	stats_user="$(uci_get main.stats_user)" || stats_user="admin"
	stats_password="$(uci_get main.stats_password)" || stats_password="secubox"
	data_path="$(uci_get main.data_path)" || data_path="$DATA_PATH"
	memory_limit="$(uci_get main.memory_limit)" || memory_limit="256M"
	maxconn="$(uci_get main.maxconn)" || maxconn="4096"
	log_level="$(uci_get main.log_level)" || log_level="warning"
	default_backend="$(uci_get main.default_backend)" || default_backend="default_luci"
	waf_enabled="$(uci_get main.waf_enabled)" || waf_enabled="0"
	waf_backend="$(uci_get main.waf_backend)" || waf_backend="mitmproxy_inspector"

	CERTS_PATH="$data_path/certs"
	CONFIG_PATH="$data_path/config"

	ensure_dir "$data_path"
	ensure_dir "$CERTS_PATH"
	ensure_dir "$CONFIG_PATH"
}

# Usage
usage() {
	cat <<EOF
SecuBox HAProxy Controller

Usage: $(basename $0) <command> [options]

Container Commands:
  install             Setup HAProxy LXC container
  uninstall           Remove container (keeps config)
  update              Update HAProxy in container
  status              Show service status

Configuration:
  generate            Generate haproxy.cfg from UCI
  validate            Validate configuration
  reload              Reload HAProxy config (no downtime)

Virtual Hosts:
  vhost list          List all virtual hosts
  vhost add <domain>  Add virtual host
  vhost remove <domain> Remove virtual host
  vhost sync          Sync vhosts to config

Backends:
  backend list        List all backends
  backend add <name>  Add backend
  backend remove <name> Remove backend

Servers:
  server list <backend>           List servers in backend
  server add <backend> <addr:port> Add server to backend
  server remove <backend> <name>  Remove server

Certificates:
  cert list           List certificates
  cert add <domain>   Request ACME certificate
  cert import <domain> <cert> <key> Import certificate
  cert renew [domain] Renew certificate(s)
  cert remove <domain> Remove certificate

Path ACLs:
  path list           List all path ACLs
  path sync <prefix> <host>  Auto-generate path ACLs from backends
                             (e.g., path sync /gk2 secubox.in)
  path add <pattern> <backend> <host>  Add path ACL
  path remove <name>  Remove path ACL
  path clear <prefix> Remove all path ACLs with prefix

Service Commands:
  service-run         Run in foreground (for init)
  service-stop        Stop service

Stats:
  stats               Show HAProxy stats
  connections         Show active connections

EOF
}

# ===========================================
# LXC Container Management
# ===========================================

lxc_running() {
	lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
}

lxc_exists() {
	[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
}

lxc_stop() {
	if lxc_running; then
		log_info "Stopping HAProxy container..."
		lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
		sleep 2
	fi
}

lxc_create_rootfs() {
	log_info "Creating Alpine rootfs for HAProxy..."

	ensure_dir "$LXC_PATH/$LXC_NAME"

	local arch="x86_64"
	case "$(uname -m)" in
		aarch64) arch="aarch64" ;;
		armv7l)  arch="armv7" ;;
	esac

	local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/$arch/alpine-minirootfs-3.21.2-$arch.tar.gz"
	local rootfs_tar="/tmp/alpine-haproxy.tar.gz"

	log_info "Downloading Alpine rootfs..."
	wget -q -O "$rootfs_tar" "$alpine_url" || {
		log_error "Failed to download Alpine rootfs"
		return 1
	}

	log_info "Extracting rootfs..."
	ensure_dir "$LXC_ROOTFS"
	tar -xzf "$rootfs_tar" -C "$LXC_ROOTFS" || {
		log_error "Failed to extract rootfs"
		return 1
	}
	rm -f "$rootfs_tar"

	# Configure Alpine
	cat > "$LXC_ROOTFS/etc/resolv.conf" << 'EOF'
nameserver 1.1.1.1
nameserver 8.8.8.8
EOF

	cat > "$LXC_ROOTFS/etc/apk/repositories" << 'EOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
EOF

	# Install HAProxy
	log_info "Installing HAProxy..."
	chroot "$LXC_ROOTFS" /bin/sh -c "
		apk update
		apk add --no-cache haproxy openssl curl socat lua5.4 lua5.4-socket
	" || {
		log_error "Failed to install HAProxy"
		return 1
	}

	log_info "Rootfs created successfully"
}

lxc_create_config() {
	load_config

	local arch="x86_64"
	case "$(uname -m)" in
		aarch64) arch="aarch64" ;;
		armv7l)  arch="armhf" ;;
	esac

	local mem_bytes=$(echo "$memory_limit" | sed 's/M/000000/;s/G/000000000/')

	cat > "$LXC_CONFIG" << EOF
# HAProxy LXC Configuration
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_ROOTFS
lxc.arch = $arch

# Network: use host network for binding ports
lxc.net.0.type = none

# Mount points - avoid cgroup:mixed which causes failures on some systems
lxc.mount.entry = $data_path opt/haproxy none bind,create=dir 0 0

# Disable seccomp for compatibility
lxc.seccomp.profile =

# TTY/PTY settings
lxc.tty.max = 0
lxc.pty.max = 256

# Auto device nodes
lxc.autodev = 1

# Environment
lxc.environment = HTTP_PORT=$http_port
lxc.environment = HTTPS_PORT=$https_port
lxc.environment = STATS_PORT=$stats_port

# Init command
lxc.init.cmd = /opt/start-haproxy.sh
EOF

	log_info "LXC config created"
}

lxc_run() {
	load_config
	lxc_stop

	if ! lxc_exists; then
		log_error "Container not installed. Run: haproxyctl install"
		return 1
	fi

	lxc_create_config

	# Ensure start script exists
	local start_script="$LXC_ROOTFS/opt/start-haproxy.sh"
	cat > "$start_script" << 'STARTEOF'
#!/bin/sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

CONFIG_FILE="/opt/haproxy/config/haproxy.cfg"
LOG_FILE="/opt/haproxy/startup.log"

# Log all output
exec >>"$LOG_FILE" 2>&1
echo "=== HAProxy startup: $(date) ==="
echo "Config: $CONFIG_FILE"
ls -la /opt/haproxy/
ls -la /opt/haproxy/certs/ 2>/dev/null || echo "No certs dir"

# Clean up legacy certificate files - only .pem files should exist
# HAProxy loads all files from certs directory, and extra files cause errors
if [ -d "/opt/haproxy/certs" ]; then
    for pem in /opt/haproxy/certs/*.pem; do
        [ -f "$pem" ] || continue
        base="${pem%.pem}"
        # Remove any associated .crt, .key, .fullchain.pem, .crt.key files
        rm -f "${base}.crt" "${base}.key" "${base}.crt.key" "${base}.fullchain.pem" 2>/dev/null
    done
fi

# Wait for config
if [ ! -f "$CONFIG_FILE" ]; then
    echo "[haproxy] Config not found, generating default..."
    mkdir -p /opt/haproxy/config
    cat > "$CONFIG_FILE" << 'CFGEOF'
global
    log stdout format raw local0
    maxconn 4096
    stats socket /var/run/haproxy.sock mode 660 level admin expose-fd listeners
    stats timeout 30s

defaults
    mode http
    log global
    option httplog
    option dontlognull
    timeout connect 5s
    timeout client 30s
    timeout server 30s

frontend stats
    bind *:8404
    mode http
    stats enable
    stats uri /stats
    stats refresh 10s
    stats admin if TRUE

frontend http-in
    bind *:80
    mode http
    default_backend default_luci

backend default_luci
    mode http
    balance roundrobin
    server luci 192.168.255.1:8081 check
CFGEOF
fi

# Validate config first
echo "[haproxy] Validating config..."
haproxy -c -f "$CONFIG_FILE"
RC=$?
echo "[haproxy] Validation exit code: $RC"
if [ $RC -ne 0 ]; then
    echo "[haproxy] Config validation failed!"
    exit 1
fi

echo "[haproxy] Starting HAProxy..."
exec haproxy -f "$CONFIG_FILE" -W -db
STARTEOF
	chmod +x "$start_script"

	# Generate config before starting
	generate_config

	log_info "Starting HAProxy container..."
	exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}

lxc_exec() {
	if ! lxc_running; then
		log_error "Container not running"
		return 1
	fi
	lxc-attach -n "$LXC_NAME" -- "$@"
}

# ===========================================
# Certificate List Generation (for multi-cert SNI)
# ===========================================

# Generate crt-list file for multi-certificate SNI support
# This is more reliable than directory mode for multi-domain setups
generate_certs_list() {
	local certs_list="$CERTS_PATH/certs.list"
	local container_certs_path="/opt/haproxy/certs"

	log_info "Generating certificate list file..."

	# Start fresh
	> "$certs_list"

	# Process each .pem file
	for cert_file in "$CERTS_PATH"/*.pem; do
		[ -f "$cert_file" ] || continue

		local filename=$(basename "$cert_file")
		local container_cert_path="$container_certs_path/$filename"

		# Extract domain names from certificate (CN and SANs)
		local domains=$(openssl x509 -in "$cert_file" -noout -text 2>/dev/null | \
			grep -A1 "Subject Alternative Name" | \
			tail -n1 | \
			sed 's/DNS://g' | \
			tr ',' '\n' | \
			tr -d ' ')

		# If no SANs, try to get CN
		if [ -z "$domains" ]; then
			domains=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | \
				sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')
		fi

		if [ -n "$domains" ]; then
			# For each domain, add an entry with SNI filter
			# Format: /path/to/cert.pem [sni_filter]
			echo "$domains" | while read -r domain; do
				[ -n "$domain" ] || continue
				# Handle wildcard certificates - the SNI filter should match the pattern
				echo "$container_cert_path $domain" >> "$certs_list"
			done
		else
			# Fallback: add cert without SNI filter (will be default)
			log_warn "No domain found in certificate: $filename"
			echo "$container_cert_path" >> "$certs_list"
		fi
	done

	# Deduplicate entries (same cert may have multiple SANs)
	if [ -f "$certs_list" ]; then
		sort -u "$certs_list" > "$certs_list.tmp"
		mv "$certs_list.tmp" "$certs_list"

		local count=$(wc -l < "$certs_list")
		log_info "Generated certs.list with $count entries"
	else
		log_warn "No certificates found in $CERTS_PATH"
	fi
}

# ===========================================
# Configuration Generation
# ===========================================

generate_config() {
	load_config

	local cfg_file="$CONFIG_PATH/haproxy.cfg"

	log_info "Generating HAProxy configuration..."

	# Generate certs.list for multi-certificate SNI support
	generate_certs_list

	# Global section
	cat > "$cfg_file" << EOF
# HAProxy Configuration - Generated by SecuBox
# DO NOT EDIT - Use UCI configuration

global
    log stdout format raw local0 $log_level
    maxconn $maxconn
    stats socket /var/run/haproxy.sock mode 660 level admin expose-fd listeners
    stats timeout 30s
    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
    tune.ssl.default-dh-param 2048

EOF

	# Defaults section
	local mode=$(uci_get defaults.mode) || mode="http"
	local timeout_connect=$(uci_get defaults.timeout_connect) || timeout_connect="5s"
	local timeout_client=$(uci_get defaults.timeout_client) || timeout_client="30s"
	local timeout_server=$(uci_get defaults.timeout_server) || timeout_server="30s"

	cat >> "$cfg_file" << EOF
defaults
    mode $mode
    log global
    option httplog
    option dontlognull
    option forwardfor
    timeout connect $timeout_connect
    timeout client $timeout_client
    timeout server $timeout_server
    timeout http-request 10s
    timeout http-keep-alive 10s
    retries 3

EOF

	# Stats frontend
	if [ "$stats_enabled" = "1" ]; then
		cat >> "$cfg_file" << EOF
frontend stats
    bind *:$stats_port
    mode http
    stats enable
    stats uri /stats
    stats refresh 10s
    stats auth $stats_user:$stats_password
    stats admin if TRUE

EOF
	fi

	# Generate frontends from UCI
	_generate_frontends >> "$cfg_file"

	# Generate backends from UCI
	_generate_backends >> "$cfg_file"

	log_info "Configuration generated: $cfg_file"
}

_generate_frontends() {
	# HTTP Frontend
	cat << EOF
frontend http-in
    bind *:$http_port,[::]:$http_port
    mode http

    # ACME challenge routing (no HAProxy restart needed for cert issuance)
    acl is_acme_challenge path_beg /.well-known/acme-challenge/
    use_backend acme_challenge if is_acme_challenge

EOF

	# Add HTTPS redirect rules for vhosts with ssl_redirect
	config_load haproxy
	config_foreach _add_ssl_redirect vhost

	# Add path-based ACLs BEFORE vhost ACLs (path rules take precedence)
	# Two-phase: collect then emit sorted by pattern length (longest first)
	rm -f "$PATH_ACL_TMPFILE"
	config_foreach _collect_path_acl acl
	_emit_sorted_path_acls

	# Add vhost ACLs for HTTP (specific domains first, then wildcards)
	config_foreach _add_vhost_acl vhost "http" "exact"
	config_foreach _add_vhost_acl vhost "http" "suffix"

	echo "    default_backend $default_backend"
	echo ""

	# HTTPS Frontend (if certificates exist)
	# Use container path /opt/haproxy/certs/ (not host path)
	local CONTAINER_CERTS_PATH="/opt/haproxy/certs"
	local CERTS_LIST_FILE="$CERTS_PATH/certs.list"
	if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then
		# Use crt-list for proper multi-certificate SNI matching
		# Falls back to directory mode if certs.list doesn't exist
		if [ -f "$CERTS_LIST_FILE" ] && [ -s "$CERTS_LIST_FILE" ]; then
			cat << EOF
frontend https-in
    bind *:$https_port,[::]:$https_port ssl crt-list $CONTAINER_CERTS_PATH/certs.list alpn h2,http/1.1
    mode http
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Real-IP %[src]
EOF
		else
			# Fallback to directory mode if no certs.list
			cat << EOF
frontend https-in
    bind *:$https_port,[::]:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1
    mode http
    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Real-IP %[src]
EOF
		fi
		# Add path-based ACLs BEFORE vhost ACLs (path rules take precedence)
		# Two-phase: collect then emit sorted by pattern length (longest first)
		rm -f "$PATH_ACL_TMPFILE"
		config_foreach _collect_path_acl acl
		_emit_sorted_path_acls

		# Add vhost ACLs for HTTPS (specific domains first, then wildcards)
		config_foreach _add_vhost_acl vhost "https" "exact"
		config_foreach _add_vhost_acl vhost "https" "suffix"

		echo "    default_backend $default_backend"
		echo ""
	fi
}

_add_ssl_redirect() {
	local section="$1"
	local enabled domain ssl_redirect

	config_get enabled "$section" enabled "0"
	[ "$enabled" = "1" ] || return

	config_get domain "$section" domain
	config_get ssl_redirect "$section" ssl_redirect "0"

	[ -n "$domain" ] || return
	[ "$ssl_redirect" = "1" ] || return

	local acl_name=$(echo "$domain" | tr '.' '_' | tr '-' '_')
	echo "    acl host_${acl_name} hdr(host) -i $domain"
	echo "    http-request redirect scheme https code 301 if host_${acl_name} !{ ssl_fc } !is_acme_challenge"
}

# Path ACL collection temp file
PATH_ACL_TMPFILE="/tmp/haproxy_path_acls.$$"

# Collect path-based ACLs from UCI 'acl' sections into temp file for sorting
# Format: pattern_length|section|type|pattern|backend|host
_collect_path_acl() {
	local section="$1"
	local enabled type pattern backend host priority

	config_get enabled "$section" enabled "1"
	[ "$enabled" = "1" ] || return

	config_get type "$section" type
	config_get pattern "$section" pattern
	config_get backend "$section" backend
	config_get host "$section" host
	config_get priority "$section" priority "100"

	# Validate required fields
	[ -n "$type" ] || return
	[ -n "$pattern" ] || return
	[ -n "$backend" ] || return

	# Calculate pattern length for sorting (longer patterns first)
	local pattern_len=${#pattern}

	# Write to temp file: length|section|type|pattern|backend|host
	echo "${pattern_len}|${section}|${type}|${pattern}|${backend}|${host}" >> "$PATH_ACL_TMPFILE"
}

# Emit sorted path ACLs (longest patterns first for correct matching)
# This ensures /gk2/evolution matches before /gk2
_emit_sorted_path_acls() {
	[ -f "$PATH_ACL_TMPFILE" ] || return

	local seen_hosts=""

	# Sort by pattern length (descending, numeric) and emit
	sort -t'|' -k1 -rn "$PATH_ACL_TMPFILE" | while IFS='|' read -r len section type pattern backend host; do
		local acl_name=$(echo "$section" | tr '.' '_' | tr '-' '_')
		local host_acl_name=""

		# If host is specified, emit host ACL only once
		if [ -n "$host" ]; then
			host_acl_name=$(echo "$host" | tr '.' '_' | tr '-' '_')
			# Check if we've already emitted this host ACL
			case "$seen_hosts" in
				*"|$host_acl_name|"*) ;;
				*)
					echo "    acl host_${host_acl_name} hdr(host) -i $host"
					seen_hosts="${seen_hosts}|${host_acl_name}|"
					;;
			esac
		fi

		# Generate path ACL based on type
		case "$type" in
			path_beg)
				echo "    acl ${acl_name} path_beg $pattern"
				;;
			path_end)
				echo "    acl ${acl_name} path_end $pattern"
				;;
			path)
				echo "    acl ${acl_name} path $pattern"
				;;
			path_reg)
				echo "    acl ${acl_name} path_reg $pattern"
				;;
			path_dir)
				echo "    acl ${acl_name} path_dir $pattern"
				;;
		esac

		# Generate use_backend rule (use WAF backend if enabled)
		local effective_backend="$backend"
		config_get waf_bypass "$section" waf_bypass "0"
	[ "$waf_enabled" = "1" ] && [ "$waf_bypass" != "1" ] && effective_backend="$waf_backend"
		if [ -n "$host_acl_name" ]; then
			echo "    use_backend $effective_backend if host_${host_acl_name} ${acl_name}"
		else
			echo "    use_backend $effective_backend if ${acl_name}"
		fi
	done

	rm -f "$PATH_ACL_TMPFILE"
}

_add_vhost_acl() {
	local section="$1"
	local proto="$2"
	local filter="${3:-all}"  # Filter: exact, suffix, regex, or all
	local enabled domain backend ssl match_type

	config_get enabled "$section" enabled "0"
	[ "$enabled" = "1" ] || return

	config_get domain "$section" domain
	config_get backend "$section" backend
	config_get match_type "$section" match_type "exact"

	# Filter by match_type if specified (to process specific vhosts before wildcards)
	if [ "$filter" != "all" ]; then
		# For "exact" filter, also include regex (both are specific, not wildcard)
		if [ "$filter" = "exact" ]; then
			[ "$match_type" = "suffix" ] && return
		elif [ "$filter" = "suffix" ]; then
			[ "$match_type" != "suffix" ] && return
		fi
	fi

	# Validate backend is not IP:port (common misconfiguration)
	case "$backend" in
		*:*) log_warn "Vhost $section has IP:port backend , should be backend name"; return ;;
	esac
	config_get ssl "$section" ssl "0"

	[ -n "$domain" ] || return
	[ -n "$backend" ] || return

	# For HTTP frontend, skip SSL-only vhosts
	[ "$proto" = "http" ] && [ "$ssl" = "1" ] && return

	local acl_name=$(echo "$domain" | tr "." "_" | tr "-" "_" | tr "*" "wildcard")

	# Handle different match types
	case "$match_type" in
		suffix)
			# Suffix match for wildcard subdomains (e.g., .gk2.secubox.in)
			echo "    acl host_${acl_name} hdr(host) -m end -i $domain"
			;;
		regex)
			# Regex match
			echo "    acl host_${acl_name} hdr(host) -m reg -i $domain"
			;;
		*)
			# Exact match (default)
			echo "    acl host_${acl_name} hdr(host) -i $domain"
			;;
	esac
	# Use WAF backend if enabled, otherwise use original backend
	local effective_backend="$backend"
	config_get waf_bypass "$section" waf_bypass "0"
	[ "$waf_enabled" = "1" ] && [ "$waf_bypass" != "1" ] && effective_backend="$waf_backend"
	echo "    use_backend $effective_backend if host_${acl_name}"
}

_generate_backends() {
	config_load haproxy

	# ACME challenge backend (for certificate issuance without HAProxy restart)
	# Serves /.well-known/acme-challenge/ from /var/www/acme-challenge/
	cat << EOF

backend acme_challenge
    mode http
    server acme_webroot 192.168.255.1:8402 check

EOF

	# Track which backends are generated
	_generated_backends=""

	# Generate each backend from UCI
	config_foreach _generate_backend backend

	# Collect all backends referenced by vhosts
	_referenced_backends=""
	_collect_vhost_backend() {
		local section="$1"
		local enabled backend
		config_get enabled "$section" enabled "0"
		[ "$enabled" = "1" ] || return
		config_get backend "$section" backend
		[ -n "$backend" ] && _referenced_backends="$_referenced_backends $backend"
	}
	config_foreach _collect_vhost_backend vhost

	# Add default_backend to referenced list
	_referenced_backends="$_referenced_backends $default_backend"

	# Generate fallback backends for any referenced but not generated
	# These common backends route to uhttpd on the host
	for backend_name in default_luci luci apps; do
		# Check if this backend is referenced
		echo "$_referenced_backends" | grep -qw "$backend_name" || continue
		# Check if already generated
		echo "$_generated_backends" | grep -qw "$backend_name" && continue
		# Generate fallback
		cat << EOF

backend $backend_name
    mode http
    balance roundrobin
    server $backend_name 192.168.255.1:8081 check
EOF
	done
}

_generate_backend() {
	local section="$1"
	local enabled name mode balance health_check health_check_uri http_request

	config_get enabled "$section" enabled "0"
	[ "$enabled" = "1" ] || return

	config_get name "$section" name "$section"
	config_get mode "$section" mode "http"
	config_get balance "$section" balance "roundrobin"
	config_get health_check "$section" health_check ""
	config_get health_check_uri "$section" health_check_uri ""

	# Track generated backend
	_generated_backends="$_generated_backends $name"

	echo ""
	echo "backend $name"
	echo "    mode $mode"

	# Check for http-request directives (always as list)
	local has_http_request_return=0
	local has_http_requests=0
	_emit_and_check_http_request() {
		local val="$1"
		has_http_requests=1
		echo "    http-request $val"
		case "$val" in
			return*) has_http_request_return=1 ;;
		esac
	}
	config_list_foreach "$section" http_request _emit_and_check_http_request

	# If any http-request was a "return" directive, skip servers
	[ "$has_http_request_return" = "1" ] && return

	echo "    balance $balance"

	# Health check configuration
	if [ -n "$health_check" ]; then
		echo "    option $health_check"
		# If health_check_uri specified, use HTTP check with GET method
		if [ -n "$health_check_uri" ]; then
			echo "    http-check send meth GET uri $health_check_uri"
			echo "    http-check expect status 200"
		fi
	fi

	# Check if there are separate server sections for this backend
	local has_server_sections=0
	_check_server_sections() {
		local srv_section="$1"
		local srv_backend
		config_get srv_backend "$srv_section" backend
		config_get srv_enabled "$srv_section" enabled "0"
		if [ "$srv_backend" = "$name" ] && [ "$srv_enabled" = "1" ]; then
			has_server_sections=1
		fi
	}
	config_foreach _check_server_sections server

	# Add inline server ONLY if no separate server sections exist
	# This prevents duplicate server names
	if [ "$has_server_sections" = "0" ]; then
		local server_line
		config_get server_line "$section" server ""
		[ -n "$server_line" ] && echo "    server $server_line"
	fi

	# Add servers from separate server UCI sections
	config_foreach _add_server_to_backend server "$name"
}

_add_server_to_backend() {
	local section="$1"
	local target_backend="$2"
	local backend server_name address port weight check enabled

	config_get backend "$section" backend
	[ "$backend" = "$target_backend" ] || return

	config_get enabled "$section" enabled "0"
	[ "$enabled" = "1" ] || return

	config_get server_name "$section" name "$section"
	config_get address "$section" address
	config_get port "$section" port "80"
	config_get weight "$section" weight "100"
	config_get check "$section" check "1"

	[ -n "$address" ] || return

	# Validate address - if it's a hostname (not IP), try to resolve it
	if ! echo "$address" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
		# It's a hostname, try to resolve it
		local resolved_ip=""
		resolved_ip=$(nslookup "$address" 2>/dev/null | awk '/^Address: / { print $2; exit }')
		if [ -z "$resolved_ip" ]; then
			# Try getent as fallback
			resolved_ip=$(getent hosts "$address" 2>/dev/null | awk '{print $1; exit}')
		fi
		if [ -z "$resolved_ip" ]; then
			log_warn "Cannot resolve hostname '$address' for server $server_name in backend $target_backend - skipping"
			return
		fi
		# Use the resolved IP instead
		log_debug "Resolved $address to $resolved_ip"
		address="$resolved_ip"
	fi

	local check_opt=""
	[ "$check" = "1" ] && check_opt="check"

	echo "    server $server_name $address:$port weight $weight $check_opt"
}

# ===========================================
# Certificate Management
# ===========================================

# Check if certificate is from Let's Encrypt Production (not Staging)
cert_is_production() {
	local cert_file="$1"
	[ -f "$cert_file" ] || return 1

	# Check the issuer - staging certs have "(STAGING)" in the issuer
	local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null)
	if echo "$issuer" | grep -qi "staging\|test\|fake"; then
		return 1  # Staging certificate
	fi

	# Check for Let's Encrypt production issuers
	if echo "$issuer" | grep -qiE "Let's Encrypt|R3|R10|R11|E1|E2|ISRG"; then
		return 0  # Production certificate
	fi

	# Check if it's a self-signed or other CA
	return 0  # Assume production for other CAs
}

# Validate certificate publicly using external service
cert_validate_public() {
	local domain="$1"
	local timeout=10

	# Try to connect and verify the certificate
	if command -v curl >/dev/null 2>&1; then
		if curl -sS --max-time "$timeout" -o /dev/null "https://$domain" 2>/dev/null; then
			return 0
		fi
	fi

	# Fallback: use openssl s_client
	if command -v openssl >/dev/null 2>&1; then
		local result=$(echo | timeout "$timeout" openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -dates 2>/dev/null)
		if [ -n "$result" ]; then
			return 0
		fi
	fi

	return 1
}

# Get certificate info
cert_info() {
	local cert_file="$1"
	[ -f "$cert_file" ] || return 1

	local subject=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | sed 's/subject=//')
	local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | sed 's/issuer=//')
	local not_after=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
	local not_before=$(openssl x509 -in "$cert_file" -noout -startdate 2>/dev/null | cut -d= -f2)

	echo "Subject: $subject"
	echo "Issuer: $issuer"
	echo "Valid From: $not_before"
	echo "Valid Until: $not_after"

	if cert_is_production "$cert_file"; then
		echo "Type: PRODUCTION (publicly trusted)"
	else
		echo "Type: STAGING/TEST (NOT publicly trusted!)"
	fi
}

# Verify and report certificate status
cmd_cert_verify() {
	load_config

	local domain="$1"
	if [ -z "$domain" ]; then
		echo "Usage: haproxyctl cert verify <domain>"
		return 1
	fi

	local cert_file="$CERTS_PATH/$domain.pem"
	if [ ! -f "$cert_file" ]; then
		log_error "Certificate not found: $cert_file"
		return 1
	fi

	echo "Certificate Information for $domain:"
	echo "======================================"
	cert_info "$cert_file"
	echo ""

	# Check if it's production
	if ! cert_is_production "$cert_file"; then
		log_warn "This is a STAGING certificate - NOT trusted by browsers!"
		log_warn "To get a production certificate, ensure staging='0' in config and re-issue"
		return 1
	fi

	# Try public validation
	echo "Public Validation:"
	if cert_validate_public "$domain"; then
		log_info "Certificate is publicly valid and accessible"
		return 0
	else
		log_warn "Could not verify certificate publicly"
		log_warn "Ensure DNS points to this server and port 443 is accessible"
		return 1
	fi
}

cmd_cert_list() {
	load_config

	echo "Certificates in $CERTS_PATH:"
	echo "----------------------------"

	if [ -d "$CERTS_PATH" ]; then
		for cert in "$CERTS_PATH"/*.pem; do
			[ -f "$cert" ] || continue
			local name=$(basename "$cert" .pem)
			local expiry=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | cut -d= -f2)
			local type_icon="✅"
			if ! cert_is_production "$cert"; then
				type_icon="⚠️ STAGING"
			fi
			echo "  $name - Expires: ${expiry:-Unknown} $type_icon"
		done
	else
		echo "  No certificates found"
	fi

	# Show current mode
	local staging=$(uci -q get haproxy.acme.staging)
	echo ""
	if [ "$staging" = "1" ]; then
		echo "⚠️  ACME Mode: STAGING (certificates will NOT be trusted by browsers)"
		echo "   To use production: uci set haproxy.acme.staging='0' && uci commit haproxy"
	else
		echo "✅ ACME Mode: PRODUCTION (certificates will be publicly trusted)"
	fi
}

# Check if we have local BIND for a zone
has_local_bind_zone() {
	local domain="$1"
	local zone_file="/etc/bind/zones/${domain}.zone"
	[ -f "$zone_file" ] && command -v rndc >/dev/null 2>&1
}

# Add TXT record to local BIND zone
bind_add_txt() {
	local subdomain="$1"
	local zone="$2"
	local value="$3"
	local zone_file="/etc/bind/zones/${zone}.zone"

	[ ! -f "$zone_file" ] && return 1

	# Update serial (format: YYYYMMDDNN)
	local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
	local today=$(date +%Y%m%d)
	local new_serial

	if [ -n "$current_serial" ]; then
		local serial_date="${current_serial:0:8}"
		local serial_num="${current_serial:8:2}"
		if [ "$serial_date" = "$today" ]; then
			serial_num=$((10#$serial_num + 1))
			new_serial=$(printf "%s%02d" "$today" "$serial_num")
		else
			new_serial="${today}01"
		fi
		sed -i "s/$current_serial/$new_serial/" "$zone_file"
	fi

	# Add TXT record (before the ; IPv6 comment or at end)
	local record="$subdomain IN TXT \"$value\""
	if grep -q "; IPv6" "$zone_file"; then
		sed -i "/; IPv6/i $record" "$zone_file"
	else
		echo "$record" >> "$zone_file"
	fi

	# Reload BIND
	rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null
	log_info "Added TXT record: $subdomain.$zone -> $value"
}

# Remove TXT record from local BIND zone
bind_remove_txt() {
	local subdomain="$1"
	local zone="$2"
	local value="$3"
	local zone_file="/etc/bind/zones/${zone}.zone"

	[ ! -f "$zone_file" ] && return 1

	# Remove the specific TXT record
	sed -i "/$subdomain IN TXT \"$value\"/d" "$zone_file"

	# Update serial
	local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
	if [ -n "$current_serial" ]; then
		local today=$(date +%Y%m%d)
		local serial_date="${current_serial:0:8}"
		local serial_num="${current_serial:8:2}"
		if [ "$serial_date" = "$today" ]; then
			serial_num=$((10#$serial_num + 1))
		else
			serial_num=1
		fi
		local new_serial=$(printf "%s%02d" "$today" "$serial_num")
		sed -i "s/$current_serial/$new_serial/" "$zone_file"
	fi

	rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null
	log_info "Removed TXT record: $subdomain.$zone"
}

# Get DNS provider configuration for DNS-01 challenge
get_dns_provider_env() {
	local provider=$(uci -q get dns-provider.main.provider)
	[ -z "$provider" ] && return 1

	case "$provider" in
		local|bind)
			# Using local BIND - acme.sh will use our custom hook
			echo "dns_local_bind"
			;;
		gandi)
			local api_key=$(uci -q get dns-provider.gandi.api_key)
			[ -z "$api_key" ] && return 1
			export GANDI_LIVEDNS_KEY="$api_key"
			echo "dns_gandi_livedns"
			;;
		ovh)
			local app_key=$(uci -q get dns-provider.ovh.app_key)
			local app_secret=$(uci -q get dns-provider.ovh.app_secret)
			local consumer_key=$(uci -q get dns-provider.ovh.consumer_key)
			local endpoint=$(uci -q get dns-provider.ovh.endpoint)
			[ -z "$app_key" ] || [ -z "$app_secret" ] || [ -z "$consumer_key" ] && return 1
			export OVH_AK="$app_key"
			export OVH_AS="$app_secret"
			export OVH_CK="$consumer_key"
			export OVH_END="${endpoint:-ovh-eu}"
			echo "dns_ovh"
			;;
		cloudflare)
			local api_token=$(uci -q get dns-provider.cloudflare.api_token)
			[ -z "$api_token" ] && return 1
			export CF_Token="$api_token"
			echo "dns_cloudflare"
			;;
		*)
			return 1
			;;
	esac
	return 0
}

# Create acme.sh DNS hook for local BIND
create_local_dns_hook() {
	local hook_dir="/etc/acme/dnsapi"
	ensure_dir "$hook_dir"

	cat > "$hook_dir/dns_local_bind.sh" << 'HOOK'
#!/bin/sh
# acme.sh DNS hook for local BIND zones

dns_local_bind_add() {
    local fulldomain="$1"
    local txtvalue="$2"

    # Extract zone from domain (e.g., _acme-challenge.gk2.secubox.in -> secubox.in)
    local zone=$(echo "$fulldomain" | awk -F. '{print $(NF-1)"."$NF}')
    local subdomain="${fulldomain%.$zone}"

    local zone_file="/etc/bind/zones/${zone}.zone"
    [ ! -f "$zone_file" ] && { echo "Zone file not found: $zone_file"; return 1; }

    # Update serial
    local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
    if [ -n "$current_serial" ]; then
        local today=$(date +%Y%m%d)
        local serial_date="${current_serial:0:8}"
        local serial_num="${current_serial:8:2}"
        if [ "$serial_date" = "$today" ]; then
            serial_num=$((10#$serial_num + 1))
        else
            serial_num=1
        fi
        local new_serial=$(printf "%s%02d" "$today" "$serial_num")
        sed -i "s/$current_serial/$new_serial/" "$zone_file"
    fi

    # Add TXT record
    local record="$subdomain IN TXT \"$txtvalue\""
    if grep -q "; IPv6" "$zone_file"; then
        sed -i "/; IPv6/i $record" "$zone_file"
    else
        echo "$record" >> "$zone_file"
    fi

    # Reload BIND (use kill -HUP since rndc may not be configured)
    local bind_pid=$(pidof named 2>/dev/null)
    if [ -n "$bind_pid" ]; then
        kill -HUP "$bind_pid" 2>/dev/null
    fi
    rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null || true
    echo "Added: $fulldomain TXT $txtvalue"

    # Wait for DNS propagation
    sleep 5
    return 0
}

dns_local_bind_rm() {
    local fulldomain="$1"
    local txtvalue="$2"

    local zone=$(echo "$fulldomain" | awk -F. '{print $(NF-1)"."$NF}')
    local subdomain="${fulldomain%.$zone}"
    local zone_file="/etc/bind/zones/${zone}.zone"

    [ ! -f "$zone_file" ] && return 0

    # Remove the TXT record
    sed -i "/$subdomain IN TXT \"$txtvalue\"/d" "$zone_file"

    # Update serial
    local current_serial=$(grep -oP '\d{10}(?=\s*;\s*Serial)' "$zone_file" 2>/dev/null)
    if [ -n "$current_serial" ]; then
        local today=$(date +%Y%m%d)
        local serial_date="${current_serial:0:8}"
        local serial_num="${current_serial:8:2}"
        if [ "$serial_date" = "$today" ]; then
            serial_num=$((10#$serial_num + 1))
        else
            serial_num=1
        fi
        local new_serial=$(printf "%s%02d" "$today" "$serial_num")
        sed -i "s/$current_serial/$new_serial/" "$zone_file"
    fi

    # Reload BIND
    local bind_pid=$(pidof named 2>/dev/null)
    if [ -n "$bind_pid" ]; then
        kill -HUP "$bind_pid" 2>/dev/null
    fi
    rndc reload "$zone" 2>/dev/null || rndc reload 2>/dev/null || true
    echo "Removed: $fulldomain TXT $txtvalue"
    return 0
}
HOOK
	chmod +x "$hook_dir/dns_local_bind.sh"
}

cmd_cert_add() {
	require_root
	load_config

	local domain="$1"
	[ -z "$domain" ] && { log_error "Domain required"; return 1; }

	# Detect if this is a wildcard certificate request
	local is_wildcard=0
	local base_domain="$domain"
	case "$domain" in
		\*.*)
			is_wildcard=1
			base_domain="${domain#\*.}"
			log_info "Wildcard certificate requested for $domain"
			;;
	esac

	# Ensure HAProxy config has ACME backend (for webroot mode)
	log_info "Ensuring HAProxy config is up to date..."
	generate_config

	# Ensure firewall allows HTTP/HTTPS from WAN (required for ACME)
	log_info "Checking firewall rules..."
	firewall_ensure_haproxy_rules "$http_port" "$https_port"

	local email=$(uci_get acme.email)
	local staging=$(uci_get acme.staging)
	local key_type_raw=$(uci_get acme.key_type) || key_type_raw="ec-256"

	# Convert key type for acme.sh (rsa-4096 → 4096, ec-256 stays ec-256)
	local key_type="$key_type_raw"
	case "$key_type_raw" in
		rsa-*) key_type="${key_type_raw#rsa-}" ;;  # rsa-4096 → 4096
		RSA-*) key_type="${key_type_raw#RSA-}" ;;
	esac

	[ -z "$email" ] && { log_error "ACME email not configured. Set in LuCI > Services > HAProxy > Settings"; return 1; }

	# For wildcard certs, DNS-01 challenge is REQUIRED
	local dns_plugin=""
	local use_local_bind=0
	if [ "$is_wildcard" = "1" ]; then
		# First check if we have local BIND for this zone
		# Extract parent zone from domain (e.g., *.gk2.secubox.in -> secubox.in)
		# Use awk instead of rev which isn't available on OpenWrt
		local parent_zone=$(echo "$base_domain" | awk -F. '{print $(NF-1)"."$NF}')
		if has_local_bind_zone "$parent_zone"; then
			log_info "Local BIND zone found for $parent_zone"
			use_local_bind=1
			dns_plugin="dns_local_bind"
			create_local_dns_hook
		else
			# Try external DNS provider
			dns_plugin=$(get_dns_provider_env)
		fi

		if [ -z "$dns_plugin" ]; then
			log_error "DNS provider not configured. Wildcard certificates require DNS-01 challenge."
			log_error "Options:"
			log_error "  1. Use local BIND: ensure /etc/bind/zones/<zone>.zone exists"
			log_error "  2. Configure external DNS: LuCI > Services > DNS Provider"
			log_error "     uci set dns-provider.main.provider='gandi'"
			log_error "     uci set dns-provider.gandi.api_key='YOUR_API_KEY'"
			log_error "     uci commit dns-provider"
			return 1
		fi
		log_info "Using DNS-01 challenge with $dns_plugin"
	fi

	# Warn about staging mode
	if [ "$staging" = "1" ]; then
		log_warn "=========================================="
		log_warn "STAGING MODE ENABLED!"
		log_warn "Certificate will NOT be trusted by browsers"
		log_warn "To use production: uci set haproxy.acme.staging='0' && uci commit haproxy"
		log_warn "=========================================="
		sleep 2
	else
		log_info "Using Let's Encrypt PRODUCTION (certificates will be publicly trusted)"
	fi

	log_info "Requesting certificate for $domain..."

	local staging_flag=""
	[ "$staging" = "1" ] && staging_flag="--staging"

	# Find acme.sh - check OpenWrt location first, then PATH
	local ACME_SH=""
	if [ -x "/usr/lib/acme/client/acme.sh" ]; then
		ACME_SH="/usr/lib/acme/client/acme.sh"
	elif command -v acme.sh >/dev/null 2>&1; then
		ACME_SH="acme.sh"
	fi

	if [ -n "$ACME_SH" ]; then
		# Set acme.sh home directory
		export LE_WORKING_DIR="/etc/acme"
		export LE_CONFIG_HOME="/etc/acme"
		ensure_dir "$LE_WORKING_DIR"

		# Register account if needed
		if [ ! -f "$LE_WORKING_DIR/account.conf" ]; then
			log_info "Registering ACME account..."
			"$ACME_SH" --register-account -m "$email" --server letsencrypt $staging_flag --home "$LE_WORKING_DIR" || true
		fi

		local acme_result=0

		if [ "$is_wildcard" = "1" ]; then
			# DNS-01 challenge for wildcard certificates
			log_info "Issuing wildcard certificate via DNS-01 challenge..."
			log_info "This will create a TXT record at _acme-challenge.$base_domain"

			"$ACME_SH" --issue -d "$domain" -d "$base_domain" \
				--server letsencrypt \
				--dns "$dns_plugin" \
				--keylength "$key_type" \
				$staging_flag \
				--home "$LE_WORKING_DIR" || acme_result=$?
		else
			# HTTP-01 challenge for regular domains (webroot mode)
			# Setup webroot for ACME challenges (HAProxy routes /.well-known/acme-challenge/ here)
			local ACME_WEBROOT="/var/www/acme-challenge"
			ensure_dir "$ACME_WEBROOT/.well-known/acme-challenge"
			chmod 755 "$ACME_WEBROOT" "$ACME_WEBROOT/.well-known" "$ACME_WEBROOT/.well-known/acme-challenge"

			# Start simple webserver for ACME challenges (if not already running)
			local ACME_PORT=8402
			if ! netstat -tln 2>/dev/null | grep -q ":$ACME_PORT "; then
				log_info "Starting ACME challenge webserver on port $ACME_PORT..."
				# Use busybox httpd (available on OpenWrt)
				start-stop-daemon -S -b -x /usr/sbin/httpd -- -p $ACME_PORT -h "$ACME_WEBROOT" -f 2>/dev/null || \
					busybox httpd -p $ACME_PORT -h "$ACME_WEBROOT" &
				sleep 1
			fi

			# Ensure HAProxy is running with ACME backend
			if ! lxc_running; then
				log_info "Starting HAProxy..."
				/etc/init.d/haproxy start 2>/dev/null || true
				sleep 2
			fi

			# Issue certificate using webroot mode (NO HAProxy restart needed!)
			log_info "Issuing certificate (webroot mode - HAProxy stays running)..."
			"$ACME_SH" --issue -d "$domain" \
				--server letsencrypt \
				--webroot "$ACME_WEBROOT" \
				--keylength "$key_type" \
				$staging_flag \
				--home "$LE_WORKING_DIR" || acme_result=$?
		fi

		# acme.sh returns 0 on success, 2 on "skip/already valid" - both are OK
		# Install the certificate to our certs path
		# For wildcard, use sanitized filename (replace * with _wildcard_)
		local cert_filename="$domain"
		case "$domain" in
			\*.*) cert_filename="_wildcard_.${domain#\*.}" ;;
		esac

		if [ "$acme_result" -eq 0 ] || [ "$acme_result" -eq 2 ]; then
			log_info "Installing certificate..."
			"$ACME_SH" --install-cert -d "$domain" \
				--home "$LE_WORKING_DIR" \
				--cert-file "$CERTS_PATH/$cert_filename.crt" \
				--key-file "$CERTS_PATH/$cert_filename.key" \
				--fullchain-file "$CERTS_PATH/$cert_filename.fullchain.pem" \
				--reloadcmd "/etc/init.d/haproxy reload" 2>/dev/null || true

			# HAProxy needs combined file: fullchain + private key
			log_info "Creating combined PEM for HAProxy..."
			cat "$CERTS_PATH/$cert_filename.fullchain.pem" "$CERTS_PATH/$cert_filename.key" > "$CERTS_PATH/$cert_filename.pem"
			chmod 600 "$CERTS_PATH/$cert_filename.pem"

			# Clean up intermediate files - HAProxy only needs the .pem file
			rm -f "$CERTS_PATH/$cert_filename.crt" "$CERTS_PATH/$cert_filename.key" "$CERTS_PATH/$cert_filename.fullchain.pem" "$CERTS_PATH/$cert_filename.crt.key" 2>/dev/null

			# Reload HAProxy to pick up new cert
			log_info "Reloading HAProxy to use new certificate..."
			/etc/init.d/haproxy reload 2>/dev/null || true
		fi

		# Check if certificate was created
		if [ ! -f "$CERTS_PATH/$cert_filename.pem" ]; then
			if [ "$is_wildcard" = "1" ]; then
				log_error "Wildcard certificate issuance failed. Check:"
				log_error "  1. DNS provider API credentials are correct"
				log_error "  2. You have permission to modify DNS for $base_domain"
				log_error "  3. DNS propagation may take time (try again in a few minutes)"
			else
				log_error "Certificate issuance failed. Check:"
				log_error "  1. Domain $domain points to this server's public IP"
				log_error "  2. Port 80 is accessible from internet"
				log_error "  3. HAProxy is running with ACME backend (haproxyctl generate)"
			fi
			return 1
		fi
		log_info "Certificate ready: $CERTS_PATH/$cert_filename.pem"

		# Update domain variable for UCI storage
		domain="$cert_filename"
	elif command -v certbot >/dev/null 2>&1; then
		certbot certonly --standalone -d "$domain" \
			--email "$email" --agree-tos -n \
			--http-01-port "$http_port" $staging_flag || {
			log_error "Certbot failed"
			return 1
		}

		# Copy to HAProxy certs dir
		local le_path="/etc/letsencrypt/live/$domain"
		cat "$le_path/fullchain.pem" "$le_path/privkey.pem" > "$CERTS_PATH/$domain.pem"
	else
		log_error "No ACME client found. Install: opkg install acme acme-acmesh"
		return 1
	fi

	chmod 600 "$CERTS_PATH/$domain.pem"

	# Verify certificate type (production vs staging)
	echo ""
	if cert_is_production "$CERTS_PATH/$domain.pem"; then
		log_info "✅ Certificate is from PRODUCTION CA (publicly trusted)"
	else
		log_warn "⚠️  Certificate is from STAGING CA (NOT publicly trusted!)"
		log_warn "   Browsers will show security warnings for this certificate"
		log_warn "   To get a production certificate:"
		log_warn "   1. uci set haproxy.acme.staging='0'"
		log_warn "   2. uci commit haproxy"
		log_warn "   3. haproxyctl cert remove $domain"
		log_warn "   4. haproxyctl cert add $domain"
	fi

	# Show certificate info
	echo ""
	cert_info "$CERTS_PATH/$domain.pem"

	# Add to UCI
	local section="cert_$(echo "$domain" | tr '.-' '__')"
	uci set haproxy.$section=certificate
	uci set haproxy.$section.domain="$domain"
	uci set haproxy.$section.type="acme"
	uci set haproxy.$section.enabled="1"
	uci commit haproxy

	log_info "Certificate installed for $domain"

	# Offer to verify publicly if production
	if cert_is_production "$CERTS_PATH/$domain.pem"; then
		echo ""
		log_info "To verify the certificate is working publicly, run:"
		log_info "  haproxyctl cert verify $domain"
	fi
}

cmd_cert_import() {
	require_root
	load_config

	local domain="$1"
	local cert_file="$2"
	local key_file="$3"

	[ -z "$domain" ] && { log_error "Domain required"; return 1; }
	[ -z "$cert_file" ] && { log_error "Certificate file required"; return 1; }
	[ -z "$key_file" ] && { log_error "Key file required"; return 1; }

	[ -f "$cert_file" ] || { log_error "Certificate file not found"; return 1; }
	[ -f "$key_file" ] || { log_error "Key file not found"; return 1; }

	# Combine cert and key for HAProxy
	cat "$cert_file" "$key_file" > "$CERTS_PATH/$domain.pem"
	chmod 600 "$CERTS_PATH/$domain.pem"

	# Add to UCI
	uci set haproxy.cert_${domain//[.-]/_}=certificate
	uci set haproxy.cert_${domain//[.-]/_}.domain="$domain"
	uci set haproxy.cert_${domain//[.-]/_}.type="manual"
	uci set haproxy.cert_${domain//[.-]/_}.enabled="1"
	uci commit haproxy

	log_info "Certificate imported for $domain"
}

# ===========================================
# Virtual Host Management
# ===========================================

cmd_vhost_list() {
	load_config

	echo "Virtual Hosts:"
	echo "--------------"

	config_load haproxy
	config_foreach _print_vhost vhost
}

_print_vhost() {
	local section="$1"
	local enabled domain backend ssl ssl_redirect acme

	config_get domain "$section" domain
	config_get backend "$section" backend
	config_get enabled "$section" enabled "0"
	config_get ssl "$section" ssl "0"
	config_get ssl_redirect "$section" ssl_redirect "0"
	config_get acme "$section" acme "0"

	local status="disabled"
	[ "$enabled" = "1" ] && status="enabled"

	local flags=""
	[ "$ssl" = "1" ] && flags="${flags}SSL "
	[ "$ssl_redirect" = "1" ] && flags="${flags}REDIRECT "
	[ "$acme" = "1" ] && flags="${flags}ACME "

	printf "  %-30s -> %-20s [%s] %s\n" "$domain" "$backend" "$status" "$flags"
}

cmd_vhost_add() {
	require_root
	load_config

	local domain="$1"
	local backend="$2"

	[ -z "$domain" ] && { log_error "Domain required"; return 1; }
	[ -z "$backend" ] && backend="fallback"

	local section="vhost_${domain//[.-]/_}"

	uci set haproxy.$section=vhost
	uci set haproxy.$section.domain="$domain"
	uci set haproxy.$section.backend="$backend"
	uci set haproxy.$section.ssl="1"
	uci set haproxy.$section.ssl_redirect="1"
	uci set haproxy.$section.acme="1"
	uci set haproxy.$section.enabled="1"
	uci commit haproxy

	log_info "Virtual host added: $domain -> $backend"

	# Regenerate GK2 Hub landing page if generator exists
	[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
}

cmd_vhost_remove() {
	require_root

	local domain="$1"
	[ -z "$domain" ] && { log_error "Domain required"; return 1; }

	local section="vhost_${domain//[.-]/_}"
	uci delete haproxy.$section 2>/dev/null
	uci commit haproxy

	log_info "Virtual host removed: $domain"

	# Regenerate GK2 Hub landing page if generator exists
	[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
}

# ===========================================
# Backend Management
# ===========================================

cmd_backend_list() {
	load_config

	echo "Backends:"
	echo "---------"

	config_load haproxy
	config_foreach _print_backend backend
}

_print_backend() {
	local section="$1"
	local enabled name mode balance

	config_get name "$section" name "$section"
	config_get enabled "$section" enabled "0"
	config_get mode "$section" mode "http"
	config_get balance "$section" balance "roundrobin"

	local status="disabled"
	[ "$enabled" = "1" ] && status="enabled"

	printf "  %-20s mode=%-6s balance=%-12s [%s]\n" "$name" "$mode" "$balance" "$status"
}

cmd_backend_add() {
	require_root

	local name="$1"
	[ -z "$name" ] && { log_error "Backend name required"; return 1; }

	local section="backend_${name//[.-]/_}"

	uci set haproxy.$section=backend
	uci set haproxy.$section.name="$name"
	uci set haproxy.$section.mode="http"
	uci set haproxy.$section.balance="roundrobin"
	uci set haproxy.$section.enabled="1"
	uci commit haproxy

	log_info "Backend added: $name"
}

cmd_server_add() {
	require_root

	local backend="$1"
	local addr_port="$2"
	local server_name="$3"

	[ -z "$backend" ] && { log_error "Backend name required"; return 1; }
	[ -z "$addr_port" ] && { log_error "Address:port required"; return 1; }

	local address=$(echo "$addr_port" | cut -d: -f1)
	local port=$(echo "$addr_port" | cut -d: -f2)
	[ -z "$port" ] && port="80"
	[ -z "$server_name" ] && server_name="srv_$(echo $address | tr '.' '_')_$port"

	local section="server_${server_name//[.-]/_}"

	uci set haproxy.$section=server
	uci set haproxy.$section.backend="$backend"
	uci set haproxy.$section.name="$server_name"
	uci set haproxy.$section.address="$address"
	uci set haproxy.$section.port="$port"
	uci set haproxy.$section.weight="100"
	uci set haproxy.$section.check="1"
	uci set haproxy.$section.enabled="1"
	uci commit haproxy

	log_info "Server added: $server_name ($address:$port) to backend $backend"
}

# ===========================================
# Commands
# ===========================================

cmd_install() {
	require_root
	load_config

	log_info "Installing HAProxy..."

	has_lxc || { log_error "LXC not installed"; exit 1; }

	if ! lxc_exists; then
		lxc_create_rootfs || exit 1
	fi

	lxc_create_config || exit 1

	log_info "Installation complete!"
	log_info ""
	log_info "Next steps:"
	log_info "  1. Enable: uci set haproxy.main.enabled=1 && uci commit haproxy"
	log_info "  2. Add vhost: haproxyctl vhost add example.com backend_name"
	log_info "  3. Start: /etc/init.d/haproxy start"
}

cmd_status() {
	load_config

	local enabled=$(uci_get main.enabled)
	local running="no"
	lxc_running && running="yes"

	cat << EOF
HAProxy Status
==============
Enabled:     $([ "$enabled" = "1" ] && echo "yes" || echo "no")
Running:     $running
HTTP Port:   $http_port
HTTPS Port:  $https_port
Stats Port:  $stats_port
Stats URL:   http://localhost:$stats_port/stats

Container:   $LXC_NAME
Rootfs:      $LXC_ROOTFS
Config:      $CONFIG_PATH/haproxy.cfg
Certs:       $CERTS_PATH
EOF
}

cmd_reload() {
	require_root

	if ! lxc_running; then
		log_error "Container not running"
		return 1
	fi

	generate_config

	log_info "Reloading HAProxy configuration..."
	# Copy generated config to container's /etc/haproxy/ (HAProxy reads from there)
	lxc_exec cp /opt/haproxy/config/haproxy.cfg /etc/haproxy/haproxy.cfg
	# HAProxy in master-worker mode (-W) reloads gracefully on SIGUSR2
	# Fallback to SIGHUP if USR2 fails
	lxc_exec killall -USR2 haproxy 2>/dev/null || \
		lxc_exec killall -HUP haproxy 2>/dev/null || \
		log_error "Could not signal HAProxy for reload"

	log_info "Reload complete"

	# Regenerate GK2 Hub landing page if generator exists
	[ -x /usr/bin/gk2hub-generate ] && /usr/bin/gk2hub-generate >/dev/null 2>&1 &
}

cmd_validate() {
	load_config
	generate_config

	log_info "Validating configuration..."

	if lxc_running; then
		lxc_exec haproxy -c -f /opt/haproxy/config/haproxy.cfg
	else
		# Validate locally if possible
		if [ -f "$CONFIG_PATH/haproxy.cfg" ]; then
			log_info "Config file: $CONFIG_PATH/haproxy.cfg"
			head -50 "$CONFIG_PATH/haproxy.cfg"
		fi
	fi
}

cmd_stats() {
	if ! lxc_running; then
		log_error "Container not running"
		return 1
	fi

	lxc_exec sh -c "echo 'show stat' | socat stdio /var/run/haproxy.sock" 2>/dev/null || \
		curl -s "http://localhost:$stats_port/stats;csv"
}

cmd_service_run() {
	require_root
	load_config

	has_lxc || { log_error "LXC not installed"; exit 1; }
	lxc_run
}

cmd_service_stop() {
	require_root
	lxc_stop
}

# ===========================================
# Path ACL Management
# ===========================================

cmd_path_list() {
	config_load haproxy
	echo "Path ACLs:"
	echo "=========================================="
	printf "%-20s %-25s %-25s %s\n" "NAME" "PATTERN" "BACKEND" "HOST"
	echo "------------------------------------------"
	config_foreach _print_path_acl acl
}

_print_path_acl() {
	local section="$1"
	local enabled pattern backend host type
	config_get enabled "$section" enabled "1"
	[ "$enabled" = "1" ] || return
	config_get type "$section" type
	config_get pattern "$section" pattern
	config_get backend "$section" backend
	config_get host "$section" host
	[ -n "$pattern" ] || return
	printf "%-20s %-25s %-25s %s\n" "$section" "$pattern" "$backend" "$host"
}

cmd_path_sync() {
	local prefix="${1:-/gk2}"
	local host="${2:-secubox.in}"

	log_info "Syncing path ACLs: prefix=$prefix host=$host"

	# Collect backend names
	local backends=""
	config_load haproxy

	_collect_backend_name() {
		local section="$1"
		local name enabled
		config_get name "$section" name "$section"
		config_get enabled "$section" enabled "1"
		[ "$enabled" = "1" ] || return

		# Skip system backends
		case "$name" in
			fallback|acme_challenge|end_of_internet|vortex_*|luci_default) return ;;
		esac

		backends="$backends $name"
	}

	config_foreach _collect_backend_name backend

	log_info "Found backends: $backends"

	# Generate path ACL for each backend
	local added=0
	for backend in $backends; do
		# Extract short name from backend (e.g., metablog_gk2 -> gk2, streamlit_evolution -> evolution)
		local short_name=""
		case "$backend" in
			metablog_*)    short_name="${backend#metablog_}" ;;
			streamlit_*)   short_name="${backend#streamlit_}" ;;
			jellyfin_*)    short_name="${backend#jellyfin_}" ;;
			*)             short_name="$backend" ;;
		esac

		local acl_name="path_$(echo "${prefix#/}" | tr '/' '_')_${short_name}"
		local pattern="${prefix}/${short_name}"

		# Check if ACL already exists
		if uci -q get haproxy.${acl_name} >/dev/null 2>&1; then
			log_debug "ACL exists: $acl_name"
			continue
		fi

		log_info "Adding: $pattern -> $backend"
		uci set haproxy.${acl_name}=acl
		uci set haproxy.${acl_name}.type="path_beg"
		uci set haproxy.${acl_name}.pattern="$pattern"
		uci set haproxy.${acl_name}.backend="$backend"
		uci set haproxy.${acl_name}.host="$host"
		uci set haproxy.${acl_name}.enabled="1"
		added=$((added + 1))
	done

	uci commit haproxy
	log_info "Added $added new path ACLs"

	# Regenerate and reload
	if [ "$added" -gt 0 ]; then
		generate_config
		cmd_reload
	fi
}

cmd_path_add() {
	local pattern="$1"
	local backend="$2"
	local host="${3:-secubox.in}"

	[ -n "$pattern" ] || { log_error "Pattern required"; return 1; }
	[ -n "$backend" ] || { log_error "Backend required"; return 1; }

	local acl_name="path_$(echo "${pattern#/}" | tr '/' '_' | tr '-' '_')"

	log_info "Adding path ACL: $pattern -> $backend (host: $host)"

	uci set haproxy.${acl_name}=acl
	uci set haproxy.${acl_name}.type="path_beg"
	uci set haproxy.${acl_name}.pattern="$pattern"
	uci set haproxy.${acl_name}.backend="$backend"
	uci set haproxy.${acl_name}.host="$host"
	uci set haproxy.${acl_name}.enabled="1"
	uci commit haproxy

	generate_config
	cmd_reload
}

cmd_path_remove() {
	local name="$1"
	[ -n "$name" ] || { log_error "ACL name required"; return 1; }

	if uci -q get haproxy.${name} >/dev/null 2>&1; then
		uci delete haproxy.${name}
		uci commit haproxy
		log_info "Removed path ACL: $name"
		generate_config
		cmd_reload
	else
		log_error "Path ACL not found: $name"
		return 1
	fi
}

cmd_path_clear() {
	local prefix="$1"
	[ -n "$prefix" ] || { log_error "Prefix required (e.g., /gk2)"; return 1; }

	local acl_prefix="path_$(echo "${prefix#/}" | tr '/' '_')"
	local removed=0

	config_load haproxy

	_check_and_remove_acl() {
		local section="$1"
		case "$section" in
			${acl_prefix}_*)
				uci delete haproxy.${section}
				removed=$((removed + 1))
				log_info "Removed: $section"
				;;
		esac
	}

	config_foreach _check_and_remove_acl acl

	if [ "$removed" -gt 0 ]; then
		uci commit haproxy
		log_info "Removed $removed path ACLs with prefix $prefix"
		generate_config
		cmd_reload
	else
		log_info "No path ACLs found with prefix $prefix"
	fi
}

# ===========================================
# Main
# ===========================================

case "${1:-}" in
	install)    shift; cmd_install "$@" ;;
	uninstall)  shift; lxc_stop; log_info "Uninstall: rm -rf $LXC_PATH/$LXC_NAME" ;;
	update)     shift; lxc_exec apk update && lxc_exec apk upgrade haproxy ;;
	status)     shift; cmd_status "$@" ;;

	generate)   shift; generate_config "$@" ;;
	validate)   shift; cmd_validate "$@" ;;
	reload)     shift; cmd_reload "$@" ;;

	vhost)
		shift
		case "${1:-}" in
			list)   shift; cmd_vhost_list "$@" ;;
			add)    shift; cmd_vhost_add "$@" ;;
			remove) shift; cmd_vhost_remove "$@" ;;
			sync)   shift; generate_config && cmd_reload ;;
			*)      echo "Usage: haproxyctl vhost {list|add|remove|sync}" ;;
		esac
		;;

	backend)
		shift
		case "${1:-}" in
			list)   shift; cmd_backend_list "$@" ;;
			add)    shift; cmd_backend_add "$@" ;;
			remove) shift; uci delete haproxy.backend_${2//[.-]/_} 2>/dev/null; uci commit haproxy ;;
			*)      echo "Usage: haproxyctl backend {list|add|remove}" ;;
		esac
		;;

	server)
		shift
		case "${1:-}" in
			list)   shift; config_load haproxy; config_foreach _print_server server "$1" ;;
			add)    shift; cmd_server_add "$@" ;;
			remove) shift; uci delete haproxy.server_${3//[.-]/_} 2>/dev/null; uci commit haproxy ;;
			*)      echo "Usage: haproxyctl server {list|add|remove} <backend> [addr:port]" ;;
		esac
		;;

	cert)
		shift
		case "${1:-}" in
			list)   shift; cmd_cert_list "$@" ;;
			add)    shift; cmd_cert_add "$@" ;;
			import) shift; cmd_cert_import "$@" ;;
			renew)  shift; cmd_cert_add "$@" ;;
			verify) shift; cmd_cert_verify "$@" ;;
			remove) shift; rm -f "$CERTS_PATH/$1.pem"; uci delete haproxy.cert_${1//[.-]/_} 2>/dev/null ;;
			*)      echo "Usage: haproxyctl cert {list|add|import|renew|verify|remove}" ;;
		esac
		;;

	path)
		shift
		case "${1:-}" in
			list)   shift; cmd_path_list "$@" ;;
			sync)   shift; cmd_path_sync "$@" ;;
			add)    shift; cmd_path_add "$@" ;;
			remove) shift; cmd_path_remove "$@" ;;
			clear)  shift; cmd_path_clear "$@" ;;
			*)      echo "Usage: haproxyctl path {list|sync|add|remove|clear}" ;;
		esac
		;;

	stats)       shift; cmd_stats "$@" ;;
	connections) shift; lxc_exec sh -c "echo 'show sess' | socat stdio /var/run/haproxy.sock" ;;

	service-run)  shift; cmd_service_run "$@" ;;
	service-stop) shift; cmd_service_stop "$@" ;;

	shell)       shift; lxc_exec /bin/sh ;;
	exec)        shift; lxc_exec "$@" ;;

	*)           usage ;;
esac
