#!/bin/bash
# haproxyctl - HAProxy Manager for SecuBox Debian
# Control script for HAProxy with LXC/Docker container support
# Three-fold architecture: Components, Status, Access
set -e

VERSION="1.1.0"

LXC_NAME="haproxy"
LXC_PATH="/var/lib/lxc/$LXC_NAME"
DOCKER_NAME="secubox-haproxy"
# Canonical SecuBox data root is /data (charter §Storage). Older boards used
# /srv/haproxy; we honour both via auto-detect, with HAPROXY_DATA_PATH as the
# explicit override. Closes the recurring cert-path bug in #286 where
# cmd_generate wrote `bind *:443 ssl crt /srv/haproxy/certs/` on boards that
# actually keep certs in /data/haproxy/certs/.
DATA_PATH="${HAPROXY_DATA_PATH:-/data/haproxy}"
[ -d "$DATA_PATH" ] || DATA_PATH="/srv/haproxy"
CONF_PATH="/etc/secubox/haproxy.toml"
CONFIG_DIR="/etc/haproxy"
CERTS_DIR="$DATA_PATH/certs"
# Operator-managed extra frontends/backends (webui-lan, gitea-ssh, custom
# metablog_* backends, etc.) live as separate files in /etc/haproxy/cfg.d/.
# cmd_generate appends them verbatim at the end of the generated cfg so they
# survive every vhost-add regen (#286).
EXTRA_CFG_DIR="${HAPROXY_EXTRA_CFG_DIR:-/etc/haproxy/cfg.d}"
STATS_SOCKET="/run/haproxy/admin.sock"
LOG_FILE="/var/log/secubox/haproxy.log"

# Mode: native, lxc, or docker
MODE="native"

# Logging
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" 2>/dev/null || echo "$*"; }
error() { log "ERROR: $*" >&2; }

# Load config
load_config() {
    if [ -f "$CONF_PATH" ]; then
        MODE=$(grep -E '^mode\s*=' "$CONF_PATH" | cut -d'"' -f2 || echo "native")
        DATA_PATH=$(grep -E '^data_path\s*=' "$CONF_PATH" | cut -d'"' -f2 || echo "$DATA_PATH")
    fi
}

# Fetch the strict-regex pattern from the secubox-haproxy API.
# Fallback: read /etc/default/secubox directly if the API socket is unreachable.
_fetch_webui_regex() {
    local sock=/run/secubox/haproxy.sock
    local regex
    if [[ -S "$sock" ]] && \
       regex=$(curl -sf --max-time 2 --unix-socket "$sock" \
                    http://localhost/webui/admin-domain 2>/dev/null \
                | jq -r '.regex // empty' 2>/dev/null) && \
       [[ -n "$regex" ]]; then
        echo "$regex"; return 0
    fi
    if [[ -f /etc/default/secubox ]]; then
        # shellcheck source=/dev/null
        . /etc/default/secubox
        if [[ -n "${SECUBOX_HOSTNAME:-}" ]]; then
            local suffix="${SECUBOX_DOMAIN_SUFFIX:-secubox.in}"
            local admin="admin.${SECUBOX_HOSTNAME}.${suffix}"
            # Escape literal dots for HAProxy regex
            echo "^${admin//./\\.}\$"
            return 0
        fi
    fi
    return 1
}

# Check container status
lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; }
lxc_exists() { [ -d "$LXC_PATH/rootfs" ]; }
docker_running() { docker ps --filter "name=$DOCKER_NAME" --format '{{.Names}}' 2>/dev/null | grep -q "$DOCKER_NAME"; }
docker_exists() { docker images "$DOCKER_NAME" --format '{{.Repository}}' 2>/dev/null | grep -q "$DOCKER_NAME"; }

# HAProxy native running check
haproxy_running() {
    case "$MODE" in
        docker) docker_running ;;
        lxc) lxc_running ;;
        *) pgrep -x haproxy >/dev/null 2>&1 ;;
    esac
}

# ─────────────────────────────────────────────────────────────────────
# CONTAINER MANAGEMENT
# ─────────────────────────────────────────────────────────────────────

cmd_install() {
    log "Installing HAProxy ($MODE mode)..."
    mkdir -p "$DATA_PATH" "$CERTS_DIR" "$CONFIG_DIR" "$(dirname $LOG_FILE)" /run/haproxy

    case "$MODE" in
        lxc)
            if lxc_exists; then
                log "LXC container already exists"
                return 0
            fi

            lxc-create -n "$LXC_NAME" -t download -- \
                -d alpine -r 3.19 -a arm64 || {
                error "Failed to create LXC container"
                return 1
            }

            cat >> "$LXC_PATH/config" << EOF

# HAProxy bind mounts
lxc.mount.entry = $CONFIG_DIR etc/haproxy none bind,create=dir 0 0
lxc.mount.entry = $CERTS_DIR srv/haproxy/certs none bind,create=dir 0 0
lxc.mount.entry = /run/haproxy run/haproxy none bind,create=dir 0 0

# Network - use macvlan for public access
lxc.net.0.type = veth
lxc.net.0.link = br-lan
lxc.net.0.flags = up
lxc.net.0.name = eth0

# Resource limits
lxc.cgroup2.memory.max = 512M
EOF

            lxc-start -n "$LXC_NAME"
            sleep 3

            lxc-attach -n "$LXC_NAME" -- sh -c '
                apk update
                apk add haproxy openssl curl
                mkdir -p /run/haproxy
            '
            ;;

        docker)
            log "Building HAProxy Docker image..."
            mkdir -p /tmp/haproxy-build
            cat > /tmp/haproxy-build/Dockerfile << 'EOF'
FROM haproxy:2.8-alpine
RUN apk add --no-cache openssl curl
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
EOF
            # Create minimal config
            cmd_generate > /tmp/haproxy-build/haproxy.cfg 2>/dev/null || \
                echo "# Minimal HAProxy config" > /tmp/haproxy-build/haproxy.cfg

            docker build -t "$DOCKER_NAME" /tmp/haproxy-build/
            rm -rf /tmp/haproxy-build
            ;;

        *)
            # Native install - assume haproxy package is installed
            if ! command -v haproxy >/dev/null; then
                error "HAProxy not found. Install with: apt install haproxy"
                return 1
            fi
            ;;
    esac

    # Generate default config
    cmd_generate

    log "HAProxy installation complete ($MODE mode)"
}

cmd_uninstall() {
    log "Uninstalling HAProxy container..."

    case "$MODE" in
        lxc)
            lxc_running && lxc-stop -n "$LXC_NAME"
            lxc_exists && lxc-destroy -n "$LXC_NAME"
            ;;
        docker)
            docker stop "$DOCKER_NAME" 2>/dev/null || true
            docker rm "$DOCKER_NAME" 2>/dev/null || true
            docker rmi "$DOCKER_NAME" 2>/dev/null || true
            ;;
    esac

    log "Container removed (config preserved in $CONFIG_DIR)"
}

cmd_start() {
    log "Starting HAProxy ($MODE mode)..."

    case "$MODE" in
        lxc)
            lxc_exists || { error "Container not installed"; return 1; }
            lxc_running || lxc-start -n "$LXC_NAME"
            sleep 2
            lxc-attach -n "$LXC_NAME" -- haproxy -f /etc/haproxy/haproxy.cfg -D
            ;;
        docker)
            docker run -d --name "$DOCKER_NAME" \
                -p 80:80 -p 443:443 -p 8404:8404 \
                -v "$CONFIG_DIR:/usr/local/etc/haproxy:ro" \
                -v "$CERTS_DIR:/certs:ro" \
                -v "/run/haproxy:/run/haproxy" \
                "$DOCKER_NAME" 2>/dev/null || \
            docker start "$DOCKER_NAME"
            ;;
        *)
            systemctl start haproxy || \
                haproxy -f /etc/haproxy/haproxy.cfg -D
            ;;
    esac

    log "HAProxy started"
}

cmd_stop() {
    log "Stopping HAProxy..."

    case "$MODE" in
        lxc)
            lxc_running && lxc-attach -n "$LXC_NAME" -- killall haproxy 2>/dev/null || true
            ;;
        docker)
            docker stop "$DOCKER_NAME" 2>/dev/null || true
            ;;
        *)
            systemctl stop haproxy 2>/dev/null || killall haproxy 2>/dev/null || true
            ;;
    esac

    log "HAProxy stopped"
}

cmd_reload() {
    log "Reloading HAProxy configuration..."

    case "$MODE" in
        lxc)
            lxc-attach -n "$LXC_NAME" -- sh -c 'kill -USR2 $(cat /run/haproxy/haproxy.pid 2>/dev/null) 2>/dev/null' || \
            lxc-attach -n "$LXC_NAME" -- haproxy -f /etc/haproxy/haproxy.cfg -sf $(pidof haproxy)
            ;;
        docker)
            docker kill -s HUP "$DOCKER_NAME" 2>/dev/null || \
            { docker stop "$DOCKER_NAME"; docker start "$DOCKER_NAME"; }
            ;;
        *)
            systemctl reload haproxy 2>/dev/null || \
                haproxy -f /etc/haproxy/haproxy.cfg -sf $(pidof haproxy)
            ;;
    esac

    log "HAProxy reloaded"
}

cmd_status() {
    local running="false"
    local mode="$MODE"

    haproxy_running && running="true"

    local vhost_count=0
    local backend_count=0
    local cert_count=0

    [ -f "$CONF_PATH" ] && {
        vhost_count=$(grep -c '^\[vhosts\.' "$CONF_PATH" 2>/dev/null || echo "0")
        backend_count=$(grep -c '^\[backends\.' "$CONF_PATH" 2>/dev/null || echo "0")
    }
    [ -d "$CERTS_DIR" ] && cert_count=$(ls -1 "$CERTS_DIR"/*.pem 2>/dev/null | wc -l || echo "0")

    cat << EOF
{
  "running": $running,
  "mode": "$mode",
  "config_dir": "$CONFIG_DIR",
  "data_path": "$DATA_PATH",
  "vhost_count": $vhost_count,
  "backend_count": $backend_count,
  "cert_count": $cert_count
}
EOF
}

# ─────────────────────────────────────────────────────────────────────
# THREE-FOLD ARCHITECTURE: Components (What)
# ─────────────────────────────────────────────────────────────────────

cmd_components() {
    local running=false
    haproxy_running && running=true

    local acme_installed=false
    command -v acme.sh >/dev/null 2>&1 && acme_installed=true

    local stats_socket_ok=false
    [ -S "$STATS_SOCKET" ] && stats_socket_ok=true

    local waf_enabled=false
    grep -q '^waf_enabled\s*=\s*true' "$CONF_PATH" 2>/dev/null && waf_enabled=true

    local vhost_count=0
    local backend_count=0
    local cert_count=0
    [ -f "$CONF_PATH" ] && vhost_count=$(grep -c '^\[vhosts\.' "$CONF_PATH" 2>/dev/null || echo "0")
    [ -f "$CONF_PATH" ] && backend_count=$(grep -c '^\[backends\.' "$CONF_PATH" 2>/dev/null || echo "0")
    [ -d "$CERTS_DIR" ] && cert_count=$(ls -1 "$CERTS_DIR"/*.pem 2>/dev/null | wc -l || echo "0")

    cat <<EOF
{
  "components": [
    {
      "name": "HAProxy",
      "type": "service",
      "description": "High-performance load balancer",
      "installed": true,
      "running": $running,
      "mode": "$MODE"
    },
    {
      "name": "Stats Socket",
      "type": "socket",
      "path": "$STATS_SOCKET",
      "description": "HAProxy admin socket",
      "running": $stats_socket_ok
    },
    {
      "name": "ACME Client",
      "type": "tool",
      "description": "Let's Encrypt certificate automation",
      "installed": $acme_installed
    },
    {
      "name": "WAF Integration",
      "type": "module",
      "description": "Web Application Firewall MITM",
      "enabled": $waf_enabled
    },
    {
      "name": "VHosts",
      "type": "config",
      "count": $vhost_count
    },
    {
      "name": "Backends",
      "type": "config",
      "count": $backend_count
    },
    {
      "name": "SSL Certificates",
      "type": "certs",
      "path": "$CERTS_DIR",
      "count": $cert_count
    }
  ]
}
EOF
}

# ─────────────────────────────────────────────────────────────────────
# THREE-FOLD ARCHITECTURE: Access (How to connect)
# ─────────────────────────────────────────────────────────────────────

cmd_access() {
    local http_port=$(grep -E '^http_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "80")
    local https_port=$(grep -E '^https_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "443")
    local stats_port=$(grep -E '^stats_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "8404")

    # Get vhosts as JSON array
    local vhosts_json="["
    local first=true
    if [ -f "$CONF_PATH" ]; then
        grep '^\[vhosts\.' "$CONF_PATH" | while read -r line; do
            local name=$(echo "$line" | sed 's/\[vhosts\.//;s/\]//')
            local section=$(sed -n "/^\[vhosts\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
            local domain=$(echo "$section" | grep -E '^domain\s*=' | cut -d'"' -f2)
            local ssl=$(echo "$section" | grep -E '^ssl\s*=' | grep -q 'true' && echo "true" || echo "false")
            local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "false" || echo "true")

            [ -n "$domain" ] && {
                $first || echo -n ","
                first=false
                echo -n "{\"domain\":\"$domain\",\"ssl\":$ssl,\"enabled\":$enabled}"
            }
        done
    fi
    vhosts_json+="]"

    cat <<EOF
{
  "ports": {
    "http": $http_port,
    "https": $https_port,
    "stats": $stats_port
  },
  "stats_url": "http://localhost:${stats_port}/stats",
  "admin_panel": "/haproxy/",
  "config_dir": "$CONFIG_DIR",
  "certs_dir": "$CERTS_DIR"
}
EOF
}

# ─────────────────────────────────────────────────────────────────────
# STATS
# ─────────────────────────────────────────────────────────────────────

cmd_stats() {
    if [ -S "$STATS_SOCKET" ]; then
        echo "show stat" | socat stdio "$STATS_SOCKET" 2>/dev/null
    else
        echo "Stats socket not available"
        return 1
    fi
}

cmd_info() {
    if [ -S "$STATS_SOCKET" ]; then
        echo "show info" | socat stdio "$STATS_SOCKET" 2>/dev/null
    else
        echo "Stats socket not available"
        return 1
    fi
}

# ─────────────────────────────────────────────────────────────────────
# VHOST MANAGEMENT
# ─────────────────────────────────────────────────────────────────────

cmd_vhost_list() {
    echo '{"vhosts":['
    local first=true

    if [ -f "$CONF_PATH" ]; then
        grep '^\[vhosts\.' "$CONF_PATH" | while read -r line; do
            local name=$(echo "$line" | sed 's/\[vhosts\.//;s/\]//')
            local section=$(sed -n "/^\[vhosts\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
            local domain=$(echo "$section" | grep -E '^domain\s*=' | cut -d'"' -f2)
            local backend=$(echo "$section" | grep -E '^backend\s*=' | cut -d'"' -f2)
            local ssl=$(echo "$section" | grep -E '^ssl\s*=' | grep -q 'true' && echo "true" || echo "false")
            local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "false" || echo "true")

            $first || echo ","
            first=false

            echo "  {\"name\": \"$name\", \"domain\": \"$domain\", \"backend\": \"$backend\", \"ssl\": $ssl, \"enabled\": $enabled}"
        done
    fi

    echo ']}'
}

cmd_vhost_add() {
    local domain="$1"
    local backend="$2"
    local ssl="${3:-false}"

    [ -z "$domain" ] || [ -z "$backend" ] && {
        error "Usage: haproxyctl vhost add <domain> <backend> [ssl]"
        return 1
    }

    local name=$(echo "$domain" | tr '.-' '_')

    # Append to config
    cat >> "$CONF_PATH" << EOF

[vhosts.$name]
domain = "$domain"
backend = "$backend"
ssl = $ssl
ssl_redirect = true
enabled = true
EOF

    log "Added vhost: $domain -> $backend"
    cmd_generate
}

cmd_vhost_remove() {
    local name="$1"
    [ -z "$name" ] && { error "Usage: haproxyctl vhost remove <name>"; return 1; }

    # Remove section from TOML (sed-based)
    sed -i "/^\[vhosts\.$name\]/,/^\[/d" "$CONF_PATH" 2>/dev/null

    log "Removed vhost: $name"
    cmd_generate
}

# ─────────────────────────────────────────────────────────────────────
# BACKEND MANAGEMENT
# ─────────────────────────────────────────────────────────────────────

cmd_backend_list() {
    echo '{"backends":['
    local first=true

    if [ -f "$CONF_PATH" ]; then
        grep '^\[backends\.' "$CONF_PATH" | while read -r line; do
            local name=$(echo "$line" | sed 's/\[backends\.//;s/\]//')
            local section=$(sed -n "/^\[backends\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
            local mode=$(echo "$section" | grep -E '^mode\s*=' | cut -d'"' -f2 || echo "http")
            local balance=$(echo "$section" | grep -E '^balance\s*=' | cut -d'"' -f2 || echo "roundrobin")

            $first || echo ","
            first=false

            echo "  {\"name\": \"$name\", \"mode\": \"$mode\", \"balance\": \"$balance\"}"
        done
    fi

    echo ']}'
}

# ─────────────────────────────────────────────────────────────────────
# CERTIFICATE MANAGEMENT
# ─────────────────────────────────────────────────────────────────────

cmd_cert_list() {
    echo '{"certificates":['
    local first=true

    if [ -d "$CERTS_DIR" ]; then
        for cert in "$CERTS_DIR"/*.pem; do
            [ -f "$cert" ] || continue
            local name=$(basename "$cert" .pem)
            local expiry=""

            # Get expiry date
            expiry=$(openssl x509 -in "$cert" -noout -enddate 2>/dev/null | sed 's/notAfter=//')

            $first || echo ","
            first=false

            echo "  {\"name\": \"$name\", \"path\": \"$cert\", \"expiry\": \"$expiry\"}"
        done
    fi

    echo ']}'
}

cmd_cert_add() {
    local domain="$1"
    local staging="${2:-}"

    [ -z "$domain" ] && { error "Usage: haproxyctl cert add <domain> [--staging]"; return 1; }

    log "Requesting certificate for $domain..."
    mkdir -p "$CERTS_DIR"

    # Use acme.sh if available
    if command -v acme.sh >/dev/null; then
        local acme_opts=""
        [ "$staging" = "--staging" ] && acme_opts="--staging"

        acme.sh --issue -d "$domain" --standalone $acme_opts && \
        acme.sh --install-cert -d "$domain" \
            --key-file "$CERTS_DIR/${domain}.key" \
            --fullchain-file "$CERTS_DIR/${domain}.crt"

        # Combine for HAProxy
        cat "$CERTS_DIR/${domain}.crt" "$CERTS_DIR/${domain}.key" > "$CERTS_DIR/${domain}.pem"
        chmod 600 "$CERTS_DIR/${domain}.pem"

        log "Certificate installed: $CERTS_DIR/${domain}.pem"
    else
        error "acme.sh not found. Install with: curl https://get.acme.sh | sh"
        return 1
    fi
}

cmd_cert_renew() {
    log "Renewing certificates..."
    if command -v acme.sh >/dev/null; then
        acme.sh --renew-all
        # Rebuild combined certs
        for crt in "$CERTS_DIR"/*.crt; do
            [ -f "$crt" ] || continue
            local name=$(basename "$crt" .crt)
            [ -f "$CERTS_DIR/${name}.key" ] && \
                cat "$crt" "$CERTS_DIR/${name}.key" > "$CERTS_DIR/${name}.pem"
        done
        cmd_reload
    else
        error "acme.sh not found"
        return 1
    fi
}

# ─────────────────────────────────────────────────────────────────────
# CONFIG GENERATION
# ─────────────────────────────────────────────────────────────────────

cmd_generate() {
    log "Generating HAProxy configuration..."

    local http_port=$(grep -E '^http_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "80")
    local https_port=$(grep -E '^https_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "443")
    local stats_port=$(grep -E '^stats_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "8404")
    local waf_enabled=$(grep -E '^waf_enabled\s*=' "$CONF_PATH" 2>/dev/null | grep -q 'true' && echo "1" || echo "0")
    local waf_port=$(grep -E '^waf_backend_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "8080")
    local waf_ip=$(grep -E '^waf_backend_ip\s*=' "$CONF_PATH" 2>/dev/null | cut -d'"' -f2 || echo "10.100.0.60")

    mkdir -p "$CONFIG_DIR" /run/haproxy "$EXTRA_CFG_DIR"

    # Build to a tempfile, validate, then atomically promote. #286 — the old
    # behaviour overwrote $CONFIG_DIR/haproxy.cfg block-by-block, so any
    # validation failure left the live cfg in a half-written state and a
    # subsequent reload picked up the broken version.
    local out
    out=$(mktemp /tmp/haproxy-gen.XXXXXX.cfg) || { error "mktemp failed"; return 1; }

    cat > "$out" << EOF
# SecuBox HAProxy Configuration (Auto-generated)
# Generated: $(date)

global
    daemon
    maxconn 4096
    stats socket /run/haproxy/admin.sock mode 660 level admin
    log /dev/log local0

defaults
    mode http
    timeout connect 5s
    timeout client 30s
    timeout server 30s
    retries 3
    option redispatch
    option httplog
    option dontlognull
    option forwardfor
    # Smart self-healing error pages (#626) — shipped by secubox-haproxy.
    errorfile 400 /etc/haproxy/secubox-errors/400.http
    errorfile 403 /etc/haproxy/secubox-errors/403.http
    errorfile 408 /etc/haproxy/secubox-errors/408.http
    errorfile 500 /etc/haproxy/secubox-errors/500.http
    errorfile 502 /etc/haproxy/secubox-errors/502.http
    errorfile 503 /etc/haproxy/secubox-errors/503.http
    errorfile 504 /etc/haproxy/secubox-errors/504.http

frontend stats
    bind *:${stats_port}
    stats enable
    stats uri /stats
    stats refresh 10s

frontend http-in
    bind *:${http_port}
    mode http
EOF

    if webui_regex=$(_fetch_webui_regex); then
        cat >> "$out" << EOF
    # WebUI Obfuscation (issue #44) — strict regex from /etc/default/secubox
    acl is_webui_admin hdr(host) -m reg $webui_regex
    use_backend webui_direct if is_webui_admin
EOF
    fi

    # Add ACLs and use_backend rules for vhosts
    if [ -f "$CONF_PATH" ]; then
        # Hoisted: source /etc/default/secubox once before the http-in loop
        _admin_skip_domain=""
        if [ -f /etc/default/secubox ]; then
            # shellcheck source=/dev/null
            . /etc/default/secubox
            if [ -n "${SECUBOX_HOSTNAME:-}" ]; then
                _admin_skip_domain="admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX:-secubox.in}"
            fi
        fi
        grep '^\[vhosts\.' "$CONF_PATH" | while read -r line; do
            local name=$(echo "$line" | sed 's/\[vhosts\.//;s/\]//')
            local section=$(sed -n "/^\[vhosts\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
            local domain=$(echo "$section" | grep -E '^domain\s*=' | cut -d'"' -f2)
            local backend=$(echo "$section" | grep -E '^backend\s*=' | cut -d'"' -f2)
            local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "0" || echo "1")
            # WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
            if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi

            # NB: use if/then/fi, not `[ ] && [ ] && { }` — under `set -e` a
            # short-circuited &&-chain (e.g. a non-SSL vhost) aborts generation.
            if [ "$enabled" = "1" ] && [ -n "$domain" ]; then
                echo "    acl host_$name hdr(host) -i $domain"
                if [ "$waf_enabled" = "1" ]; then
                    echo "    use_backend mitmproxy_inspector if host_$name"
                else
                    echo "    use_backend $backend if host_$name"
                fi
            fi
        done >> "$out"
    fi

    cat >> "$out" << EOF
    default_backend mitmproxy_inspector

frontend https-in
    bind *:${https_port} ssl crt $CERTS_DIR/
    mode http
EOF

    if webui_regex=$(_fetch_webui_regex); then
        cat >> "$out" << EOF
    # WebUI Obfuscation (issue #44) — strict regex from /etc/default/secubox
    acl is_webui_admin hdr(host) -m reg $webui_regex
    use_backend webui_direct if is_webui_admin
EOF
    fi

    # HTTPS vhost rules (same pattern)
    if [ -f "$CONF_PATH" ]; then
        # Hoisted: source /etc/default/secubox once before the https-in loop
        _admin_skip_domain=""
        if [ -f /etc/default/secubox ]; then
            # shellcheck source=/dev/null
            . /etc/default/secubox
            if [ -n "${SECUBOX_HOSTNAME:-}" ]; then
                _admin_skip_domain="admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX:-secubox.in}"
            fi
        fi
        grep '^\[vhosts\.' "$CONF_PATH" | while read -r line; do
            local name=$(echo "$line" | sed 's/\[vhosts\.//;s/\]//')
            local section=$(sed -n "/^\[vhosts\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
            local domain=$(echo "$section" | grep -E '^domain\s*=' | cut -d'"' -f2)
            local backend=$(echo "$section" | grep -E '^backend\s*=' | cut -d'"' -f2)
            local ssl=$(echo "$section" | grep -E '^ssl\s*=' | grep -q 'true' && echo "1" || echo "0")
            local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "0" || echo "1")
            # WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
            if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi

            # if/then/fi (not `&& { }`) — set -e safe for non-SSL vhosts.
            if [ "$enabled" = "1" ] && [ "$ssl" = "1" ] && [ -n "$domain" ]; then
                echo "    acl host_$name hdr(host) -i $domain"
                if [ "$waf_enabled" = "1" ]; then
                    echo "    use_backend mitmproxy_inspector if host_$name"
                else
                    echo "    use_backend $backend if host_$name"
                fi
            fi
        done >> "$out"
    fi

    cat >> "$out" << EOF
    default_backend mitmproxy_inspector

EOF

    # WAF inspector backend (mitmproxy LXC container)
    # if/then/fi (not `&& cat`) — under set -e a false test here aborts generation.
    if [ "$waf_enabled" = "1" ]; then cat >> "$out" << EOF
# WAF Inspector Backend (mitmproxy LXC at $waf_ip:$waf_port)
backend mitmproxy_inspector
    mode http
    option forwardfor
    http-request set-header X-Real-IP %[src]
    http-request set-uri http://%[req.hdr(Host)]%[path]%[query]
    server waf ${waf_ip}:${waf_port} check

EOF
    fi

    # WebUI direct backend (issue #44 — for the strict-regex ACL above)
    # Only emit if SECUBOX_HOSTNAME is set (i.e., the strict ACL was injected).
    if _fetch_webui_regex >/dev/null 2>&1; then
        cat >> "$out" << 'EOF'
backend webui_direct
    mode http
    server srv0 127.0.0.1:9080 check
EOF
    fi

    # User backends
    if [ -f "$CONF_PATH" ]; then
        grep '^\[backends\.' "$CONF_PATH" | while read -r line; do
            local name=$(echo "$line" | sed 's/\[backends\.//;s/\]//')
            # Skip backends already emitted (mitmproxy_inspector/webui_direct are
            # auto-generated above) to avoid duplicate-backend fatal errors.
            if grep -q "^backend $name\$" "$out"; then continue; fi
            local section=$(sed -n "/^\[backends\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
            local mode=$(echo "$section" | grep -E '^mode\s*=' | cut -d'"' -f2 || echo "http")
            local balance=$(echo "$section" | grep -E '^balance\s*=' | cut -d'"' -f2 || echo "roundrobin")
            local servers=$(echo "$section" | grep -E '^servers\s*=' | cut -d'[' -f2 | cut -d']' -f1 | tr -d '"' | tr ',' '\n')

            cat >> "$out" << EOF
backend $name
    mode $mode
    balance $balance
EOF
            local i=0
            echo "$servers" | while read -r srv; do
                if [ -n "$srv" ]; then
                    srv=$(echo "$srv" | tr -d ' ')
                    echo "    server srv$i $srv check" >> "$out"
                    i=$((i + 1))
                fi
            done
            echo "" >> "$out"
        done
    fi

    # Fallback backend
    cat >> "$out" << EOF
backend fallback
    mode http
    http-request deny deny_status 503
EOF

    # Make absolutely sure the canonical backends exist regardless of the
    # waf_enabled gate or user TOML — #286: vhosts can reference these even
    # when waf_enabled=0 (e.g. nginx_vhosts for non-WAF passthrough,
    # webui_direct for the SECUBOX_HOSTNAME obfuscation regex).
    grep -q '^backend nginx_vhosts$' "$out" || cat >> "$out" << EOF

backend nginx_vhosts
    mode http
    balance roundrobin
    server srv0 127.0.0.1:9080 check
EOF
    grep -q '^backend webui_direct$' "$out" || cat >> "$out" << EOF

backend webui_direct
    mode http
    server srv0 127.0.0.1:9080 check
EOF
    grep -q '^backend mitmproxy_inspector$' "$out" || cat >> "$out" << EOF

# Emitted unconditionally so use_backend mitmproxy_inspector references stay
# resolvable even with waf_enabled=0 (defense in depth — #286).
backend mitmproxy_inspector
    mode http
    balance roundrobin
    server srv0 ${waf_ip:-10.100.0.60}:${waf_port:-8080} check
EOF

    # Operator-managed extras (#286). Files in $EXTRA_CFG_DIR are appended
    # verbatim so things like the LAN webui frontend, gitea-ssh, custom
    # metablog backends, etc. survive every vhost-add regeneration.
    if [ -d "$EXTRA_CFG_DIR" ]; then
        for extra in "$EXTRA_CFG_DIR"/*.cfg; do
            [ -f "$extra" ] || continue
            printf '\n# === %s ===\n' "$(basename "$extra")" >> "$out"
            cat "$extra" >> "$out"
        done
    fi

    # Validate THEN atomic-promote (#286). The old code wrote block-by-block
    # to the live cfg and only validated at the end, leaving the file in a
    # broken state on failure (recurring "broken-by-vhost-add" backups).
    if haproxy -c -f "$out" 2>/dev/null; then
        # Drift guard (#626): refuse to overwrite a live config that has MORE
        # vhosts/backends than we just generated — that means the live cfg holds
        # entries absent from our inputs (haproxy.toml / cfg.d). Without this,
        # a successful regen would silently drop hand-maintained vhosts (kbin,
        # gitea, …). `local x=$(...)` masks grep's exit so set -e stays happy.
        if [ -f "$CONFIG_DIR/haproxy.cfg" ]; then
            local _nh=$(grep -c '^[[:space:]]*acl host_' "$out")
            local _oh=$(grep -c '^[[:space:]]*acl host_' "$CONFIG_DIR/haproxy.cfg")
            local _nb=$(grep -c '^backend ' "$out")
            local _ob=$(grep -c '^backend ' "$CONFIG_DIR/haproxy.cfg")
            if [ "${_nh:-0}" -lt "${_oh:-0}" ] || [ "${_nb:-0}" -lt "${_ob:-0}" ]; then
                error "Drift guard: generated cfg has fewer vhosts/backends than live (acl ${_nh}<${_oh} or backend ${_nb}<${_ob}) — refusing to clobber. Migrate the missing entries into haproxy.toml/cfg.d first."
                rm -f "$out"
                return 1
            fi
        fi
        install -m 0644 -o root -g root "$out" "$CONFIG_DIR/haproxy.cfg"
        rm -f "$out"
        log "Configuration generated, validated and installed: $CONFIG_DIR/haproxy.cfg"
        return 0
    else
        error "Configuration validation failed — live cfg LEFT UNTOUCHED"
        haproxy -c -f "$out" 2>&1 | tail -20 >&2
        rm -f "$out"
        return 1
    fi
}

# ─────────────────────────────────────────────────────────────────────
# MIGRATION
# ─────────────────────────────────────────────────────────────────────

cmd_migrate() {
    local source="${1:-192.168.255.1}"

    log "Migrating HAProxy config from $source..."
    mkdir -p "$DATA_PATH" "$CERTS_DIR" "$CONFIG_DIR"

    # Sync certificates
    log "Syncing certificates..."
    rsync -avz --progress "root@$source:/srv/haproxy/certs/" "$CERTS_DIR/" 2>/dev/null || \
    rsync -avz --progress "root@$source:/etc/haproxy/certs/" "$CERTS_DIR/" 2>/dev/null || true

    # Convert UCI config to TOML
    log "Converting UCI configuration to TOML..."

    cat > "$CONF_PATH" << EOF
# HAProxy Configuration (Migrated from OpenWrt)
# Generated: $(date)

[main]
mode = "native"
http_port = 80
https_port = 443
stats_port = 8404
waf_enabled = true
waf_backend_port = 8890

EOF

    # Convert vhosts
    ssh "root@$source" "uci show haproxy 2>/dev/null" | grep '=vhost$' | while read -r line; do
        local section=$(echo "$line" | cut -d'.' -f2 | cut -d'=' -f1)
        local domain=$(ssh "root@$source" "uci -q get haproxy.$section.domain" || echo "")
        local backend=$(ssh "root@$source" "uci -q get haproxy.$section.backend" || echo "")
        local ssl=$(ssh "root@$source" "uci -q get haproxy.$section.ssl" || echo "0")
        local enabled=$(ssh "root@$source" "uci -q get haproxy.$section.enabled" || echo "1")

        [ -n "$domain" ] && cat >> "$CONF_PATH" << EOF
[vhosts.$section]
domain = "$domain"
backend = "$backend"
ssl = $([ "$ssl" = "1" ] && echo "true" || echo "false")
ssl_redirect = true
enabled = $([ "$enabled" = "1" ] && echo "true" || echo "false")

EOF
    done

    # Convert backends
    ssh "root@$source" "uci show haproxy 2>/dev/null" | grep '=backend$' | while read -r line; do
        local section=$(echo "$line" | cut -d'.' -f2 | cut -d'=' -f1)
        local mode=$(ssh "root@$source" "uci -q get haproxy.$section.mode" || echo "http")
        local balance=$(ssh "root@$source" "uci -q get haproxy.$section.balance" || echo "roundrobin")
        local server=$(ssh "root@$source" "uci -q get haproxy.$section.server" || echo "")

        cat >> "$CONF_PATH" << EOF
[backends.$section]
mode = "$mode"
balance = "$balance"
servers = ["$server"]

EOF
    done

    # Count migrated items
    local vhost_count=$(grep -c '^\[vhosts\.' "$CONF_PATH" 2>/dev/null || echo "0")
    local backend_count=$(grep -c '^\[backends\.' "$CONF_PATH" 2>/dev/null || echo "0")
    local cert_count=$(ls -1 "$CERTS_DIR"/*.pem 2>/dev/null | wc -l || echo "0")

    # Generate new config
    cmd_generate

    log "Migration complete: $vhost_count vhosts, $backend_count backends, $cert_count certificates"
    echo "{\"success\": true, \"vhosts\": $vhost_count, \"backends\": $backend_count, \"certs\": $cert_count}"
}

# ─────────────────────────────────────────────────────────────────────
# WAF INTEGRATION
# ─────────────────────────────────────────────────────────────────────

cmd_waf_status() {
    local waf_enabled=$(grep -E '^waf_enabled\s*=' "$CONF_PATH" 2>/dev/null | grep -q 'true' && echo "true" || echo "false")
    local waf_ip=$(grep -E '^waf_backend_ip\s*=' "$CONF_PATH" 2>/dev/null | cut -d'"' -f2 || echo "10.100.0.60")
    local waf_port=$(grep -E '^waf_backend_port\s*=' "$CONF_PATH" 2>/dev/null | cut -d'=' -f2 | tr -d ' ' || echo "8080")
    local waf_running="false"
    local waf_container="false"

    # Check if mitmproxy container is running
    if lxc-info -n mitmproxy -s 2>/dev/null | grep -q "RUNNING"; then
        waf_container="true"
        # Check if mitmproxy service is active
        if lxc-attach -n mitmproxy -- systemctl is-active mitmproxy >/dev/null 2>&1; then
            waf_running="true"
        fi
    fi

    # Check connectivity
    local waf_reachable="false"
    if curl -s --connect-timeout 2 "http://${waf_ip}:${waf_port}/" >/dev/null 2>&1; then
        waf_reachable="true"
    fi

    cat <<EOF
{
  "enabled": $waf_enabled,
  "container_running": $waf_container,
  "service_running": $waf_running,
  "reachable": $waf_reachable,
  "ip": "$waf_ip",
  "port": $waf_port,
  "backend": "mitmproxy_inspector"
}
EOF
}

cmd_waf_enable() {
    if [ -f "$CONF_PATH" ]; then
        if grep -q '^waf_enabled' "$CONF_PATH"; then
            sed -i 's/^waf_enabled.*/waf_enabled = true/' "$CONF_PATH"
        else
            echo 'waf_enabled = true' >> "$CONF_PATH"
        fi
        log "WAF enabled in config"
        cmd_generate
        cmd_reload
    else
        error "Config file not found: $CONF_PATH"
        return 1
    fi
}

cmd_waf_disable() {
    if [ -f "$CONF_PATH" ]; then
        sed -i 's/^waf_enabled.*/waf_enabled = false/' "$CONF_PATH"
        log "WAF disabled in config"
        cmd_generate
        cmd_reload
    else
        error "Config file not found: $CONF_PATH"
        return 1
    fi
}

# ─────────────────────────────────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────────────────────────────────

load_config

case "${1:-}" in
    # Three-fold architecture
    components) cmd_components ;;
    access)     cmd_access ;;
    status)     cmd_status ;;
    # Service control
    install)    cmd_install ;;
    uninstall)  cmd_uninstall ;;
    start)      cmd_start ;;
    stop)       cmd_stop ;;
    restart)    cmd_stop; sleep 1; cmd_start ;;
    reload)     cmd_reload ;;
    stats)      cmd_stats ;;
    info)       cmd_info ;;
    generate)   cmd_generate ;;

    vhost)
        case "${2:-}" in
            list)   cmd_vhost_list ;;
            add)    cmd_vhost_add "$3" "$4" "$5" ;;
            remove) cmd_vhost_remove "$3" ;;
            *)      echo "Usage: haproxyctl vhost {list|add|remove} [args]" ;;
        esac
        ;;

    backend)
        case "${2:-}" in
            list)   cmd_backend_list ;;
            *)      echo "Usage: haproxyctl backend {list}" ;;
        esac
        ;;

    cert)
        case "${2:-}" in
            list)   cmd_cert_list ;;
            add)    cmd_cert_add "$3" "$4" ;;
            renew)  cmd_cert_renew ;;
            *)      echo "Usage: haproxyctl cert {list|add|renew} [domain]" ;;
        esac
        ;;

    migrate)    cmd_migrate "$2" ;;

    waf)
        case "${2:-}" in
            status)  cmd_waf_status ;;
            enable)  cmd_waf_enable ;;
            disable) cmd_waf_disable ;;
            *)       echo "Usage: haproxyctl waf {status|enable|disable}" ;;
        esac
        ;;

    *)
        cat << EOF
haproxyctl v$VERSION - HAProxy Manager
Three-fold architecture: Components, Status, Access

Information (Three-fold):
  components    List system components (JSON)
  status        Show status (JSON)
  access        Show ports and access info (JSON)

Service:
  install       Install HAProxy (native/lxc/docker based on config)
  uninstall     Remove HAProxy container
  start         Start HAProxy
  stop          Stop HAProxy
  restart       Restart HAProxy
  reload        Reload configuration
  stats         Show live stats
  info          Show HAProxy info

Config:
  generate      Generate HAProxy config from TOML

VHosts:
  vhost list              List vhosts
  vhost add <domain> <backend> [ssl]   Add vhost
  vhost remove <name>     Remove vhost

Backends:
  backend list            List backends

Certificates:
  cert list               List certificates
  cert add <domain> [--staging]   Request certificate
  cert renew              Renew all certificates

Migration:
  migrate [source]        Migrate from OpenWrt (default: 192.168.255.1)

WAF (mitmproxy):
  waf status              Show WAF status (JSON)
  waf enable              Enable WAF inspection
  waf disable             Disable WAF inspection

Examples:
  haproxyctl components             # JSON list of components
  haproxyctl access                 # JSON port and URL info
  haproxyctl vhost add app.ex.com backend_app true

EOF
        ;;
esac
