#!/bin/bash
#
# ╔═══════════════════════════════════════════════════════════════════╗
# ║      PAQCTL - Paqet Manager v1.0.0                                ║
# ║                                                                   ║
# ║  One-click setup for Paqet raw-socket proxy                       ║
# ║                                                                   ║
# ║  * Installs paqet binary + libpcap                                ║
# ║  * Auto-detects network config                                    ║
# ║  * Configures server or client mode                               ║
# ║  * Manages iptables rules                                         ║
# ║  * Auto-start on boot via systemd/OpenRC/SysVinit                 ║
# ║  * Easy management via CLI or interactive menu                    ║
# ║                                                                   ║
# ║  Paqet: https://git.samnet.dev/SamNet-dev/paqctl                           ║
# ╚═══════════════════════════════════════════════════════════════════╝
#
# Usage:
# curl -sL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash
#
# Or: wget paqctl.sh && sudo bash paqctl.sh
#

set -eo pipefail

# Require bash
if [ -z "$BASH_VERSION" ]; then
    echo "Error: This script requires bash. Please run with: bash $0"
    exit 1
fi

VERSION="1.0.0"

# Pinned versions for stability (update these after testing new releases)
PAQET_VERSION_PINNED="v1.0.0-alpha.17"
XRAY_VERSION_PINNED="v26.2.4"
GFK_VERSION_PINNED="v1.0.0"

PAQET_REPO="hanselime/paqet"
PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest"
INSTALL_DIR="${INSTALL_DIR:-/opt/paqctl}"
BACKUP_DIR="$INSTALL_DIR/backups"
GFK_REPO="SamNet-dev/paqctl"
GFK_BRANCH="main"
GFK_RAW_URL="https://git.samnet.dev/${GFK_REPO}/raw/branch/${GFK_BRANCH}/gfk"
GFK_DIR="$INSTALL_DIR/gfk"
MICROSOCKS_REPO="rofl0r/microsocks"
BACKEND="${BACKEND:-paqet}"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

#═══════════════════════════════════════════════════════════════════════
# Utility Functions
#═══════════════════════════════════════════════════════════════════════

print_header() {
    echo -e "${CYAN}"
    echo "╔════════════════════════════════════════════════════════════════╗"
    echo "║                  PAQCTL - Paqet Manager v${VERSION}                   ║"
    echo "║        Raw-socket encrypted proxy - bypass firewalls           ║"
    echo "╚════════════════════════════════════════════════════════════════╝"
    echo -e "${NC}"
}

log_info() {
    echo -e "${BLUE}[INFO]${NC} $1"
}

log_success() {
    echo -e "${GREEN}[✓]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[!]${NC} $1"
}

log_error() {
    echo -e "${RED}[✗]${NC} $1"
}

check_root() {
    if [ "$EUID" -ne 0 ]; then
        log_error "This script must be run as root (use sudo)"
        exit 1
    fi
}

detect_os() {
    OS="unknown"
    OS_VERSION="unknown"
    OS_FAMILY="unknown"
    HAS_SYSTEMD=false
    PKG_MANAGER="unknown"

    if [ -f /etc/os-release ]; then
        . /etc/os-release
        OS="$ID"
        OS_VERSION="${VERSION_ID:-unknown}"
    elif [ -f /etc/redhat-release ]; then
        OS="rhel"
    elif [ -f /etc/debian_version ]; then
        OS="debian"
    elif [ -f /etc/alpine-release ]; then
        OS="alpine"
    elif [ -f /etc/arch-release ]; then
        OS="arch"
    elif [ -f /etc/SuSE-release ] || [ -f /etc/SUSE-brand ]; then
        OS="opensuse"
    else
        OS=$(uname -s | tr '[:upper:]' '[:lower:]')
    fi

    case "$OS" in
        ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian)
            OS_FAMILY="debian"
            PKG_MANAGER="apt"
            ;;
        rhel|centos|fedora|rocky|almalinux|oracle|amazon|amzn)
            OS_FAMILY="rhel"
            if command -v dnf &>/dev/null; then
                PKG_MANAGER="dnf"
            else
                PKG_MANAGER="yum"
            fi
            ;;
        arch|manjaro|endeavouros|garuda)
            OS_FAMILY="arch"
            PKG_MANAGER="pacman"
            ;;
        opensuse|opensuse-leap|opensuse-tumbleweed|sles)
            OS_FAMILY="suse"
            PKG_MANAGER="zypper"
            ;;
        alpine)
            OS_FAMILY="alpine"
            PKG_MANAGER="apk"
            ;;
        *)
            OS_FAMILY="unknown"
            PKG_MANAGER="unknown"
            ;;
    esac

    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        HAS_SYSTEMD=true
    fi

    log_info "Detected: $OS ($OS_FAMILY family), Package manager: $PKG_MANAGER"
}

install_package() {
    local package="$1"
    log_info "Installing $package..."

    case "$PKG_MANAGER" in
        apt)
            apt-get update -q 2>/dev/null || log_warn "apt-get update failed, attempting install anyway..."
            if apt-get install -y -q "$package"; then
                log_success "$package installed successfully"
            else
                log_error "Failed to install $package"
                return 1
            fi
            ;;
        dnf)
            if dnf install -y -q "$package"; then
                log_success "$package installed successfully"
            else
                log_error "Failed to install $package"
                return 1
            fi
            ;;
        yum)
            if yum install -y -q "$package"; then
                log_success "$package installed successfully"
            else
                log_error "Failed to install $package"
                return 1
            fi
            ;;
        pacman)
            if pacman -Sy --noconfirm "$package"; then
                log_success "$package installed successfully"
            else
                log_error "Failed to install $package"
                return 1
            fi
            ;;
        zypper)
            if zypper install -y -n "$package"; then
                log_success "$package installed successfully"
            else
                log_error "Failed to install $package"
                return 1
            fi
            ;;
        apk)
            if apk add --no-cache "$package"; then
                log_success "$package installed successfully"
            else
                log_error "Failed to install $package"
                return 1
            fi
            ;;
        *)
            log_warn "Unknown package manager. Please install $package manually."
            return 1
            ;;
    esac
}

check_dependencies() {
    if [ "$OS_FAMILY" = "alpine" ]; then
        if ! command -v bash &>/dev/null; then
            apk add --no-cache bash 2>/dev/null
        fi
    fi

    if ! command -v curl &>/dev/null; then
        install_package curl || log_warn "Could not install curl automatically"
    fi

    if ! command -v tar &>/dev/null; then
        install_package tar || log_warn "Could not install tar automatically"
    fi

    if ! command -v ip &>/dev/null; then
        case "$PKG_MANAGER" in
            apt) install_package iproute2 || log_warn "Could not install iproute2" ;;
            dnf|yum) install_package iproute || log_warn "Could not install iproute" ;;
            pacman) install_package iproute2 || log_warn "Could not install iproute2" ;;
            zypper) install_package iproute2 || log_warn "Could not install iproute2" ;;
            apk) install_package iproute2 || log_warn "Could not install iproute2" ;;
        esac
    fi

    if ! command -v tput &>/dev/null; then
        case "$PKG_MANAGER" in
            apt) install_package ncurses-bin || log_warn "Could not install ncurses-bin" ;;
            apk) install_package ncurses || log_warn "Could not install ncurses" ;;
            *) install_package ncurses || log_warn "Could not install ncurses" ;;
        esac
    fi

    # Firewall rules: use firewalld if active, otherwise iptables
    if _is_firewalld_active; then
        log_info "firewalld detected — will use firewall-cmd for rules"
    elif ! command -v iptables &>/dev/null; then
        log_info "Installing iptables..."
        case "$PKG_MANAGER" in
            apt) install_package iptables || log_warn "Could not install iptables - firewall rules may not work" ;;
            dnf|yum) install_package iptables || log_warn "Could not install iptables" ;;
            pacman) install_package iptables || log_warn "Could not install iptables" ;;
            zypper) install_package iptables || log_warn "Could not install iptables" ;;
            apk) install_package iptables || log_warn "Could not install iptables" ;;
            *) log_warn "Please install iptables manually for firewall rules to work" ;;
        esac
    fi

    # openssl is required for GFK certificate generation
    if ! command -v openssl &>/dev/null; then
        install_package openssl || log_warn "Could not install openssl"
    fi

    # libpcap is required by paqet
    install_libpcap
}

install_libpcap() {
    log_info "Checking for libpcap..."

    # Check if already available
    if ldconfig -p 2>/dev/null | grep -q libpcap; then
        log_success "libpcap already installed"
        return 0
    fi

    case "$PKG_MANAGER" in
        apt) install_package libpcap-dev ;;
        dnf|yum) install_package libpcap-devel ;;
        pacman) install_package libpcap ;;
        zypper) install_package libpcap-devel ;;
        apk) install_package libpcap-dev ;;
        *) log_warn "Please install libpcap manually for your distribution"; return 1 ;;
    esac

    # Fedora/RHEL: ensure libpcap.so.1 symlink exists (package may only install versioned .so)
    if [ "$PKG_MANAGER" = "dnf" ] || [ "$PKG_MANAGER" = "yum" ]; then
        if ! ldconfig -p 2>/dev/null | grep -q 'libpcap\.so\.1 '; then
            local _pcap_lib
            _pcap_lib=$(find /usr/lib64 /usr/lib /lib64 /lib -name 'libpcap.so.*' -type f 2>/dev/null | head -1)
            if [ -n "$_pcap_lib" ]; then
                local _libdir
                _libdir=$(dirname "$_pcap_lib")
                if [ ! -e "${_libdir}/libpcap.so.1" ]; then
                    log_info "Creating libpcap.so.1 symlink for Fedora/RHEL compatibility"
                    ln -sf "$_pcap_lib" "${_libdir}/libpcap.so.1"
                fi
                ldconfig 2>/dev/null || true
            fi
        fi
    fi
}

detect_arch() {
    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64) echo "amd64" ;;
        aarch64|arm64) echo "arm64" ;;
        armv7l|armv7|armhf) echo "arm32" ;;
        mips64el|mips64le) echo "mips64le" ;;
        mips64) echo "mips64" ;;
        mipsel|mipsle) echo "mipsle" ;;
        mips) echo "mips" ;;
        *)
            log_error "Unsupported architecture: $arch"
            log_error "Paqet supports amd64, arm64, arm32, and MIPS variants"
            exit 1
            ;;
    esac
}

#═══════════════════════════════════════════════════════════════════════
# Input Validation Functions
#═══════════════════════════════════════════════════════════════════════

_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }

_validate_ip() {
    [[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
    local IFS='.'; set -- $1
    [ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ]
}

_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; }

_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; }

_validate_version_tag() {
    [[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]
}

#═══════════════════════════════════════════════════════════════════════
# Binary Download & Install
#═══════════════════════════════════════════════════════════════════════

# Retry helper with exponential backoff for API requests
_curl_with_retry() {
    local url="$1"
    local max_attempts="${2:-3}"
    local attempt=1
    local delay=2
    local response=""
    while [ $attempt -le $max_attempts ]; do
        response=$(curl -s --max-time 15 "$url" 2>/dev/null)
        if [ -n "$response" ]; then
            # Check for rate limit response
            if echo "$response" | grep -q '"message".*rate limit'; then
                log_warn "GitHub API rate limited, waiting ${delay}s (attempt $attempt/$max_attempts)"
                sleep $delay
                delay=$((delay * 2))
                attempt=$((attempt + 1))
                continue
            fi
            echo "$response"
            return 0
        fi
        [ $attempt -lt $max_attempts ] && sleep $delay
        delay=$((delay * 2))
        attempt=$((attempt + 1))
    done
    return 1
}

get_latest_version() {
    local response
    response=$(_curl_with_retry "$PAQET_API_URL" 3)
    if [ -z "$response" ]; then
        log_error "Failed to query GitHub API after retries"
        return 1
    fi
    local tag
    tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
    if [ -z "$tag" ]; then
        log_error "Could not determine latest paqet version"
        return 1
    fi
    if ! _validate_version_tag "$tag"; then
        log_error "Invalid version tag format: $tag"
        return 1
    fi
    echo "$tag"
}

download_paqet() {
    local version="$1"
    local arch
    arch=$(detect_arch)
    local os_name="linux"
    local ext="tar.gz"
    local filename="paqet-${os_name}-${arch}-${version}.${ext}"
    local url="https://github.com/${PAQET_REPO}/releases/download/${version}/${filename}"

    log_info "Downloading paqet ${version} for ${os_name}/${arch}..."

    if ! mkdir -p "$INSTALL_DIR/bin"; then
        log_error "Failed to create directory $INSTALL_DIR/bin"
        return 1
    fi
    local tmp_file
    tmp_file=$(mktemp "/tmp/paqet-download-XXXXXXXX.${ext}") || { log_error "Failed to create temp file"; return 1; }

    # Try curl first, fallback to wget
    local download_ok=false
    if curl -sL --max-time 180 --retry 3 --retry-delay 5 --fail -o "$tmp_file" "$url" 2>/dev/null; then
        download_ok=true
    elif command -v wget &>/dev/null; then
        log_info "curl failed, trying wget..."
        rm -f "$tmp_file"
        if wget -q --timeout=180 --tries=3 -O "$tmp_file" "$url" 2>/dev/null; then
            download_ok=true
        fi
    fi

    if [ "$download_ok" != "true" ]; then
        log_error "Failed to download: $url"
        log_error "Try manual download: wget '$url' and place binary in $INSTALL_DIR/bin/"
        rm -f "$tmp_file"
        return 1
    fi

    # Validate download
    local fsize
    fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
    if [ "$fsize" -lt 1000 ]; then
        log_error "Downloaded file is too small ($fsize bytes). Download may have failed."
        rm -f "$tmp_file"
        return 1
    fi

    # Extract
    log_info "Extracting..."
    local tmp_extract
    tmp_extract=$(mktemp -d "/tmp/paqet-extract-XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
    if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
        log_error "Failed to extract archive"
        rm -f "$tmp_file"
        rm -rf "$tmp_extract"
        return 1
    fi

    # Find the binary in extracted files
    local binary_name="paqet_${os_name}_${arch}"
    local found_binary=""
    found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
    if [ -z "$found_binary" ]; then
        # Try alternate name patterns
        found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
    fi
    if [ -z "$found_binary" ]; then
        found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)
    fi

    if [ -z "$found_binary" ]; then
        log_error "Could not find paqet binary in archive"
        rm -f "$tmp_file"
        rm -rf "$tmp_extract"
        return 1
    fi

    # Stop paqet if running to avoid "Text file busy" error
    if pgrep -f "$INSTALL_DIR/bin/paqet" &>/dev/null; then
        log_info "Stopping paqet to update binary..."
        pkill -f "$INSTALL_DIR/bin/paqet" 2>/dev/null || true
        sleep 1
    fi

    if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
        log_error "Failed to copy paqet binary to $INSTALL_DIR/bin/"
        rm -f "$tmp_file"
        rm -rf "$tmp_extract"
        return 1
    fi
    if ! chmod +x "$INSTALL_DIR/bin/paqet"; then
        log_error "Failed to make paqet binary executable"
        return 1
    fi

    # Copy example configs if they exist
    find "$tmp_extract" -name "*.yaml.example" -exec cp {} "$INSTALL_DIR/" \; 2>/dev/null || true

    rm -f "$tmp_file"
    rm -rf "$tmp_extract"

    # Verify binary runs
    if "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
        log_success "paqet ${version} installed successfully"
    else
        log_warn "paqet binary installed but version check failed (may need libpcap)"
    fi
}

#═══════════════════════════════════════════════════════════════════════
# Network Auto-Detection
#═══════════════════════════════════════════════════════════════════════

detect_network() {
    log_info "Auto-detecting network configuration..."

    # Default interface - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats
    # Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0
    # OpenVZ:   "default dev venet0 scope link"   -> $3 = venet0
    local _route_line
    _route_line=$(ip route show default 2>/dev/null | head -1)
    if [[ "$_route_line" == *" via "* ]]; then
        # Standard format with gateway
        DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}')
    elif [[ "$_route_line" == *" dev "* ]]; then
        # OpenVZ/direct format without gateway
        DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}')
    fi

    # Validate detected interface exists
    if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then
        DETECTED_IFACE=""
    fi

    if [ -z "$DETECTED_IFACE" ]; then
        # Skip loopback, docker, veth, bridge, and other virtual interfaces
        # Note: grep -v returns exit 1 if no matches, so we add || true for pipefail
        DETECTED_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '{gsub(/ /,"",$2); print $2}' | { grep -vE '^(lo|docker[0-9]|br-|veth|virbr|tun|tap|wg)' || true; } | head -1)
    fi

    # Local IP - wrap entire pipeline to prevent pipefail exit
    if [ -n "$DETECTED_IFACE" ]; then
        # Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP
        DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true )
    fi
    if [ -z "$DETECTED_IP" ]; then
        DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
        [ -z "$DETECTED_IP" ] && DETECTED_IP=$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}')
    fi

    # Gateway IP - only present in standard "via X" format, not in OpenVZ
    if [[ "$_route_line" == *" via "* ]]; then
        DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}')
    else
        DETECTED_GATEWAY=""
    fi

    # Gateway MAC
    DETECTED_GW_MAC=""
    if [ -n "$DETECTED_GATEWAY" ]; then
        # Try ip neigh first (most reliable on Linux)
        DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
        if [ -z "$DETECTED_GW_MAC" ]; then
            # Trigger ARP resolution
            ping -c 1 -W 2 "$DETECTED_GATEWAY" &>/dev/null || true
            sleep 1
            DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
        fi
        if [ -z "$DETECTED_GW_MAC" ] && command -v arp &>/dev/null; then
            # Fallback: parse arp output looking for MAC pattern
            # Note: grep returns exit 1 if no matches, so we add || true for pipefail
            DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | head -1)
        fi
    fi

    log_info "Interface: ${DETECTED_IFACE:-unknown}"
    log_info "Local IP:  ${DETECTED_IP:-unknown}"
    log_info "Gateway:   ${DETECTED_GATEWAY:-unknown}"
    log_info "GW MAC:    ${DETECTED_GW_MAC:-unknown}"
}

#═══════════════════════════════════════════════════════════════════════
# Configuration Wizard
#═══════════════════════════════════════════════════════════════════════

run_config_wizard() {
    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  PAQCTL CONFIGURATION WIZARD${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""

    # Backend selection
    echo -e "${BOLD}Select backend:${NC}"
    echo "  1. paqet       (Go/KCP, built-in SOCKS5, single binary)"
    echo "  2. gfw-knocker (Python/QUIC, port forwarding + SOCKS5)"
    echo ""
    local backend_choice
    read -p "  Enter choice [1/2]: " backend_choice < /dev/tty || true
    case "$backend_choice" in
        2) BACKEND="gfw-knocker" ;;
        *) BACKEND="paqet" ;;
    esac
    echo ""
    log_info "Selected backend: $BACKEND"
    echo ""

    # Role selection
    echo -e "${BOLD}Select role:${NC}"
    echo "  1. Server  (accept connections from clients)"
    echo "  2. Client  (connect to a server, provides SOCKS5 proxy)"
    echo ""
    local role_choice
    read -p "  Enter choice [1/2]: " role_choice < /dev/tty || true
    case "$role_choice" in
        1) ROLE="server" ;;
        2) ROLE="client" ;;
        *)
            log_warn "Invalid choice. Defaulting to server."
            ROLE="server"
            ;;
    esac
    echo ""
    log_info "Selected role: $ROLE"

    if [ "$BACKEND" = "paqet" ]; then
        _wizard_paqet
    else
        _wizard_gfk
    fi

    # Save settings
    save_settings
}

_wizard_paqet() {
    # Auto-detect network
    detect_network
    echo ""

    # Confirm/override interface
    echo -e "${BOLD}Network interface${NC} [${DETECTED_IFACE:-eth0}]:"
    read -p "  Interface: " input < /dev/tty || true
    IFACE="${input:-$DETECTED_IFACE}"
    IFACE="${IFACE:-eth0}"
    if ! _validate_iface "$IFACE"; then
        log_warn "Invalid interface name. Using eth0."
        IFACE="eth0"
    fi

    # Confirm/override local IP
    echo -e "${BOLD}Local IP${NC} [${DETECTED_IP:-auto}]:"
    read -p "  IP: " input < /dev/tty || true
    LOCAL_IP="${input:-$DETECTED_IP}"
    if [ -n "$LOCAL_IP" ] && ! _validate_ip "$LOCAL_IP"; then
        log_warn "Invalid IP format. Using detected IP."
        LOCAL_IP="$DETECTED_IP"
    fi

    # Confirm/override gateway MAC
    echo -e "${BOLD}Gateway MAC address${NC} [${DETECTED_GW_MAC:-auto}]:"
    read -p "  MAC: " input < /dev/tty || true
    GW_MAC="${input:-$DETECTED_GW_MAC}"

    if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then
        if [ -n "$GW_MAC" ]; then
            log_warn "Invalid MAC format detected."
        else
            log_error "Could not detect gateway MAC address."
        fi
        log_error "Please enter it manually (format: aa:bb:cc:dd:ee:ff)"
        read -p "  Gateway MAC: " GW_MAC < /dev/tty || true
        if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then
            log_error "Valid gateway MAC is required for paqet to function."
            exit 1
        fi
    fi

    if [ "$ROLE" = "server" ]; then
        echo ""
        echo -e "${BOLD}Listen port${NC} [8443]:"
        read -p "  Port: " input < /dev/tty || true
        LISTEN_PORT="${input:-8443}"
        if ! [[ "$LISTEN_PORT" =~ ^[0-9]+$ ]] || [ "$LISTEN_PORT" -lt 1 ] || [ "$LISTEN_PORT" -gt 65535 ]; then
            log_warn "Invalid port. Using default 8443."
            LISTEN_PORT=8443
        fi

        echo ""
        log_info "Generating encryption key..."
        ENCRYPTION_KEY=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
        if [ -z "$ENCRYPTION_KEY" ]; then
            log_warn "Could not auto-generate key. Using openssl fallback..."
            ENCRYPTION_KEY=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
        fi
        if [ -z "$ENCRYPTION_KEY" ] || [ "${#ENCRYPTION_KEY}" -lt 16 ]; then
            log_error "Failed to generate a valid encryption key"
            return 1
        fi
        echo ""
        echo -e "${GREEN}${BOLD}  Encryption Key: ${ENCRYPTION_KEY}${NC}"
        echo ""
        echo -e "${YELLOW}  IMPORTANT: Save this key! Clients need it to connect.${NC}"
        echo ""
        SOCKS_PORT=""
    else
        echo ""
        echo -e "${BOLD}Remote server address${NC} (IP:PORT):"
        read -p "  Server: " REMOTE_SERVER < /dev/tty || true
        if [ -z "$REMOTE_SERVER" ]; then
            log_error "Remote server address is required."
            exit 1
        fi

        echo ""
        echo -e "${BOLD}Encryption key${NC} (from server setup):"
        read -p "  Key: " ENCRYPTION_KEY < /dev/tty || true
        if [ -z "$ENCRYPTION_KEY" ]; then
            log_error "Encryption key is required."
            exit 1
        fi

        echo ""
        echo -e "${BOLD}SOCKS5 listen port${NC} [1080]:"
        read -p "  SOCKS port: " input < /dev/tty || true
        SOCKS_PORT="${input:-1080}"
        if ! [[ "$SOCKS_PORT" =~ ^[0-9]+$ ]] || [ "$SOCKS_PORT" -lt 1 ] || [ "$SOCKS_PORT" -gt 65535 ]; then
            log_warn "Invalid port. Using default 1080."
            SOCKS_PORT=1080
        fi
        LISTEN_PORT=""
    fi

    # Generate YAML config
    generate_config
}

_wizard_gfk() {
    if [ "$ROLE" = "server" ]; then
        # Server IP (this machine's public IP)
        detect_network
        echo ""
        echo -e "${BOLD}This server's public IP${NC} [${DETECTED_IP:-}]:"
        read -p "  IP: " input < /dev/tty || true
        GFK_SERVER_IP="${input:-$DETECTED_IP}"
        if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then
            log_error "Valid server IP is required."
            exit 1
        fi

        # VIO TCP port (must be closed to OS, raw socket handles it)
        echo ""
        echo -e "${BOLD}VIO TCP port${NC} [45000] (raw socket port, must be blocked by firewall):"
        read -p "  Port: " input < /dev/tty || true
        GFK_VIO_PORT="${input:-45000}"
        if ! _validate_port "$GFK_VIO_PORT"; then
            log_warn "Invalid port. Using default 45000."
            GFK_VIO_PORT=45000
        fi

        # QUIC port
        echo ""
        echo -e "${BOLD}QUIC tunnel port${NC} [25000]:"
        read -p "  Port: " input < /dev/tty || true
        GFK_QUIC_PORT="${input:-25000}"
        if ! _validate_port "$GFK_QUIC_PORT"; then
            log_warn "Invalid port. Using default 25000."
            GFK_QUIC_PORT=25000
        fi

        # Auth code
        echo ""
        local auto_auth
        auto_auth=$(openssl rand -base64 16 2>/dev/null | tr -d '=+/' | head -c 16)
        echo -e "${BOLD}QUIC auth code${NC} [auto-generated]:"
        read -p "  Auth code: " input < /dev/tty || true
        GFK_AUTH_CODE="${input:-$auto_auth}"
        echo ""
        echo -e "${GREEN}${BOLD}  Auth Code: ${GFK_AUTH_CODE}${NC}"
        echo ""
        echo -e "${YELLOW}  IMPORTANT: Save this auth code! Clients need it to connect.${NC}"
        echo ""

        # Port mappings
        echo -e "${BOLD}TCP port mappings${NC} (local:remote, comma-separated) [14000:443]:"
        echo -e "  ${DIM}Example: 14000:443,15000:2096,16000:10809${NC}"
        read -p "  Mappings: " input < /dev/tty || true
        GFK_PORT_MAPPINGS="${input:-14000:443}"
        MICROSOCKS_PORT=""

    else
        # Client: server IP
        echo ""
        echo -e "${BOLD}Remote server IP${NC} (server's public IP):"
        read -p "  Server IP: " GFK_SERVER_IP < /dev/tty || true
        if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then
            log_error "Valid server IP is required."
            exit 1
        fi

        # Server's VIO TCP port (what port the server is listening on)
        echo ""
        echo -e "${BOLD}Server's VIO TCP port${NC} [45000] (must match server config):"
        read -p "  Port: " input < /dev/tty || true
        GFK_VIO_PORT="${input:-45000}"
        if ! _validate_port "$GFK_VIO_PORT"; then
            log_warn "Invalid port. Using default 45000."
            GFK_VIO_PORT=45000
        fi

        # Local VIO client port (client's local binding)
        echo ""
        echo -e "${BOLD}Local VIO client port${NC} [40000]:"
        read -p "  Port: " input < /dev/tty || true
        GFK_VIO_CLIENT_PORT="${input:-40000}"
        if ! _validate_port "$GFK_VIO_CLIENT_PORT"; then
            log_warn "Invalid port. Using default 40000."
            GFK_VIO_CLIENT_PORT=40000
        fi

        # Server's QUIC port
        echo ""
        echo -e "${BOLD}Server's QUIC port${NC} [25000] (must match server config):"
        read -p "  Port: " input < /dev/tty || true
        GFK_QUIC_PORT="${input:-25000}"
        if ! _validate_port "$GFK_QUIC_PORT"; then
            log_warn "Invalid port. Using default 25000."
            GFK_QUIC_PORT=25000
        fi

        # Local QUIC client port
        echo ""
        echo -e "${BOLD}Local QUIC client port${NC} [20000]:"
        read -p "  Port: " input < /dev/tty || true
        GFK_QUIC_CLIENT_PORT="${input:-20000}"
        if ! _validate_port "$GFK_QUIC_CLIENT_PORT"; then
            log_warn "Invalid port. Using default 20000."
            GFK_QUIC_CLIENT_PORT=20000
        fi

        # Auth code (from server)
        echo ""
        echo -e "${BOLD}QUIC auth code${NC} (from server setup):"
        read -p "  Auth code: " GFK_AUTH_CODE < /dev/tty || true
        if [ -z "$GFK_AUTH_CODE" ]; then
            log_error "Auth code is required."
            exit 1
        fi

        # Port mappings (must match server)
        echo ""
        echo -e "${BOLD}TCP port mappings${NC} (must match server) [14000:443]:"
        read -p "  Mappings: " input < /dev/tty || true
        GFK_PORT_MAPPINGS="${input:-14000:443}"
    fi

    # Generate GFK config
    generate_gfk_config
}

generate_config() {
    log_info "Generating paqet configuration..."

    # Validate required fields
    if [ -z "$IFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GW_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then
        log_error "Missing required configuration fields (interface, ip, gateway_mac, or secret)"
        return 1
    fi
    if [ "$ROLE" = "server" ]; then
        if [ -z "$LISTEN_PORT" ]; then log_error "Missing listen port"; return 1; fi
    else
        if [ -z "$REMOTE_SERVER" ] || [ -z "$SOCKS_PORT" ]; then
            log_error "Missing server address or SOCKS port"
            return 1
        fi
        local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}"
        if ! _validate_ip "$_rs_ip" || ! _validate_port "$_rs_port"; then
            log_error "Server address must be valid IP:PORT (e.g. 1.2.3.4:8443)"
            return 1
        fi
    fi

    # Escape YAML special characters to prevent injection
    _escape_yaml() {
        local s="$1"
        # If value contains special chars, quote it
        if [[ "$s" =~ [:\#\[\]{}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
            s="${s//\\/\\\\}"  # Escape backslashes
            s="${s//\"/\\\"}"  # Escape double quotes
            printf '"%s"' "$s"
        else
            printf '%s' "$s"
        fi
    }

    # Ensure install directory exists
    mkdir -p "$INSTALL_DIR" || { log_error "Failed to create $INSTALL_DIR"; return 1; }

    local tmp_conf
    tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
    # Set permissions on temp file before writing (fixes race condition)
    chmod 600 "$tmp_conf" 2>/dev/null

    (
    umask 077
    local _y_iface _y_ip _y_mac _y_key _y_server _y_port
    _y_iface=$(_escape_yaml "$IFACE")
    _y_ip=$(_escape_yaml "$LOCAL_IP")
    _y_mac=$(_escape_yaml "$GW_MAC")
    _y_key=$(_escape_yaml "$ENCRYPTION_KEY")
    # Build TCP flags YAML array (default: ["PA"])
    local _tcp_local_flags _tcp_remote_flags
    _tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
    _tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')

    if [ "$ROLE" = "server" ]; then
        cat > "$tmp_conf" << EOF
role: "server"

log:
  level: "info"

listen:
  addr: ":${LISTEN_PORT}"

network:
  interface: "${_y_iface}"
  ipv4:
    addr: "${_y_ip}:${LISTEN_PORT}"
    router_mac: "${_y_mac}"
  tcp:
    local_flag: ${_tcp_local_flags}
    remote_flag: ${_tcp_remote_flags}

transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_y_key}"
EOF
    else
        local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}"
        _y_server=$(_escape_yaml "$REMOTE_SERVER")
        cat > "$tmp_conf" << EOF
role: "client"

log:
  level: "info"

socks5:
  - listen: "127.0.0.1:${SOCKS_PORT}"

network:
  interface: "${_y_iface}"
  ipv4:
    addr: "${_y_ip}:0"
    router_mac: "${_y_mac}"
  tcp:
    local_flag: ${_tcp_local_flags}
    remote_flag: ${_tcp_remote_flags}

server:
  addr: "${_y_server}"

transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_y_key}"
EOF
    fi
    )
    if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
        log_error "Failed to save configuration file"
        rm -f "$tmp_conf"
        return 1
    fi
    # Ensure final permissions (mv preserves source permissions on most systems)
    chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
    log_success "Configuration saved to $INSTALL_DIR/config.yaml"
}

save_settings() {
    # Preserve existing Telegram settings if present
    local _tg_token="" _tg_chat="" _tg_interval=6 _tg_enabled=false
    local _tg_alerts=true _tg_daily=true _tg_weekly=true _tg_label="" _tg_start_hour=0
    if [ -f "$INSTALL_DIR/settings.conf" ]; then
        # Safe settings loading without eval
        while IFS='=' read -r key value; do
            [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
            # Remove surrounding quotes and sanitize value
            value="${value#\"}"; value="${value%\"}"
            # Validate value doesn't contain dangerous characters
            if [[ "$value" =~ [\`\$\(] ]]; then
                continue  # Skip potentially dangerous values
            fi
            case "$key" in
                TELEGRAM_BOT_TOKEN) _tg_token="$value" ;;
                TELEGRAM_CHAT_ID) _tg_chat="$value" ;;
                TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && _tg_interval="$value" ;;
                TELEGRAM_ENABLED) _tg_enabled="$value" ;;
                TELEGRAM_ALERTS_ENABLED) _tg_alerts="$value" ;;
                TELEGRAM_DAILY_SUMMARY) _tg_daily="$value" ;;
                TELEGRAM_WEEKLY_SUMMARY) _tg_weekly="$value" ;;
                TELEGRAM_SERVER_LABEL) _tg_label="$value" ;;
                TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && _tg_start_hour="$value" ;;
            esac
        done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
    fi

    # Sanitize sensitive values - remove shell metacharacters and control chars
    _sanitize_value() {
        printf '%s' "$1" | tr -d '"$`\\'\''(){}[]<>|;&!\n\r\t'
    }
    local _safe_key; _safe_key=$(_sanitize_value "${ENCRYPTION_KEY:-}")
    local _safe_auth; _safe_auth=$(_sanitize_value "${GFK_AUTH_CODE:-}")
    local _tmp
    _tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
    (
    umask 077
    cat > "$_tmp" << EOF
BACKEND="${BACKEND:-paqet}"
ROLE="${ROLE}"
PAQET_VERSION="${PAQET_VERSION:-unknown}"
PAQCTL_VERSION="${VERSION}"
LISTEN_PORT="${LISTEN_PORT:-}"
SOCKS_PORT="${SOCKS_PORT:-}"
INTERFACE="${IFACE:-}"
LOCAL_IP="${LOCAL_IP:-}"
GATEWAY_MAC="${GW_MAC:-}"
ENCRYPTION_KEY="${_safe_key}"
PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}"
PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}"
REMOTE_SERVER="${REMOTE_SERVER:-}"
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
GFK_VIO_CLIENT_PORT="${GFK_VIO_CLIENT_PORT:-}"
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
GFK_QUIC_CLIENT_PORT="${GFK_QUIC_CLIENT_PORT:-}"
GFK_AUTH_CODE="${_safe_auth}"
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}"
GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}"
XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}"
MICROSOCKS_PORT="${MICROSOCKS_PORT:-}"
GFK_SERVER_IP="${GFK_SERVER_IP:-}"
GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}"
TELEGRAM_BOT_TOKEN="${_tg_token}"
TELEGRAM_CHAT_ID="${_tg_chat}"
TELEGRAM_INTERVAL=${_tg_interval}
TELEGRAM_ENABLED=${_tg_enabled}
TELEGRAM_ALERTS_ENABLED=${_tg_alerts}
TELEGRAM_DAILY_SUMMARY=${_tg_daily}
TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly}
TELEGRAM_SERVER_LABEL="${_tg_label}"
TELEGRAM_START_HOUR=${_tg_start_hour}
EOF
    )
    if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then
        log_error "Failed to save settings"
        rm -f "$_tmp"
        return 1
    fi
    chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null
    log_success "Settings saved"
}

#═══════════════════════════════════════════════════════════════════════
# Firewall Management
#═══════════════════════════════════════════════════════════════════════

_is_firewalld_active() {
    command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running
}

apply_iptables_rules() {
    local port="$1"
    if [ -z "$port" ]; then
        log_error "No port specified for iptables rules"
        return 1
    fi

    log_info "Applying firewall rules for port $port..."

    # firewalld path (Fedora/RHEL)
    if _is_firewalld_active; then
        firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
        firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            { log_error "Failed to add PREROUTING NOTRACK rule via firewalld"; return 1; }
        firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
        firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            { log_error "Failed to add OUTPUT NOTRACK rule via firewalld"; return 1; }
        firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
        firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
            { log_error "Failed to add RST DROP rule via firewalld"; return 1; }
        log_success "IPv4 firewalld rules applied"
        # IPv6
        firewall-cmd --direct --add-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        persist_iptables_rules
        return 0
    fi

    # iptables path (Debian/Ubuntu/Arch/etc.)
    modprobe iptable_raw 2>/dev/null || true
    modprobe iptable_mangle 2>/dev/null || true

    if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
        log_warn "ufw is active — ensure port $port/tcp is allowed: sudo ufw allow $port/tcp"
    fi

    local TAG="paqctl"

    if iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \
       iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \
       iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null; then
        log_info "iptables rules already in place"
    else
        iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
        iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || {
            log_error "Failed to add PREROUTING NOTRACK rule"
            return 1
        }
        iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
        iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || {
            log_error "Failed to add OUTPUT NOTRACK rule"
            return 1
        }
        iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
        iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || {
            log_error "Failed to add RST DROP rule"
            return 1
        }
        log_success "IPv4 iptables rules applied"
    fi

    if command -v ip6tables &>/dev/null; then
        local _ipv6_ok=true
        ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
            ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false
        ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
            ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false
        ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
            ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || _ipv6_ok=false
        if [ "$_ipv6_ok" = true ]; then
            log_success "IPv6 iptables rules applied"
        else
            log_warn "Some IPv6 iptables rules failed (IPv6 may not be available)"
        fi
    fi

    # Persist rules
    persist_iptables_rules
}

remove_iptables_rules() {
    local port="$1"
    if [ -z "$port" ]; then return 0; fi

    log_info "Removing firewall rules for port $port..."

    # firewalld path
    if _is_firewalld_active; then
        firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv4 filter INPUT 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv4 filter OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        # IPv6
        firewall-cmd --direct --remove-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv6 filter INPUT 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv6 filter OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        log_success "firewalld rules removed"
        return 0
    fi

    # iptables path
    local TAG="paqctl"
    iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
    iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
    iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
    iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
    iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
    iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
    if command -v ip6tables &>/dev/null; then
        ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
        ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
        ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
        ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
        ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
        ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
    fi
    log_success "iptables rules removed"
}

persist_iptables_rules() {
    if _is_firewalld_active; then
        firewall-cmd --runtime-to-permanent 2>/dev/null || true
        return 0
    fi
    if command -v netfilter-persistent &>/dev/null; then
        netfilter-persistent save 2>/dev/null || true
    elif command -v iptables-save &>/dev/null; then
        if [ -d /etc/iptables ]; then
            iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
            command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
        elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then
            mkdir -p /etc/iptables
            iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
            command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
        elif [ -d /etc/sysconfig ]; then
            iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
            command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true
        fi
    fi
}

check_iptables_rules() {
    local port="$1"
    if [ -z "$port" ]; then return 1; fi

    local ok=true

    if _is_firewalld_active; then
        firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || ok=false
        firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || ok=false
        firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || ok=false
    else
        local TAG="paqctl"
        iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false
        iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false
        iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || ok=false
    fi

    if [ "$ok" = true ]; then
        return 0
    else
        return 1
    fi
}

#═══════════════════════════════════════════════════════════════════════
# GFW-knocker Backend Functions
#═══════════════════════════════════════════════════════════════════════

install_python_deps() {
    log_info "Installing Python dependencies for GFW-knocker..."
    if ! command -v python3 &>/dev/null; then
        install_package python3
    fi
    # Ensure python3 version >= 3.10
    local pyver
    pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "0.0")
    local pymajor pyminor
    pymajor=$(echo "$pyver" | cut -d. -f1)
    pyminor=$(echo "$pyver" | cut -d. -f2)
    if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then
        log_error "Python 3.10+ required, found $pyver"
        return 1
    fi

    # Install venv support (varies by distro)
    # - Debian/Ubuntu: needs python3-venv or python3.X-venv package
    # - Fedora/RHEL/Arch/openSUSE: venv included with python3, just need pip
    # - Alpine: needs py3-pip
    case "$PKG_MANAGER" in
        apt)
            # Debian/Ubuntu needs python3-venv package (version-specific)
            local pyver_full
            pyver_full=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
            if [ -n "$pyver_full" ]; then
                install_package "python${pyver_full}-venv" || install_package "python3-venv"
            else
                install_package "python3-venv"
            fi
            ;;
        dnf)
            # Fedora/RHEL 8+: venv is included with python3, just ensure pip
            install_package "python3-pip" || true
            ;;
        yum)
            # Older RHEL/CentOS 7
            install_package "python3-pip" || true
            ;;
        pacman)
            # Arch Linux: venv included with python, pip is separate
            install_package "python-pip" || true
            ;;
        zypper)
            # openSUSE: venv included with python3
            install_package "python3-pip" || true
            ;;
        apk)
            # Alpine
            install_package "py3-pip" || true
            ;;
        *)
            # Try generic python3-venv, ignore if fails (venv may be built-in)
            install_package "python3-venv" 2>/dev/null || true
            ;;
    esac

    # Create virtual environment
    local VENV_DIR="$INSTALL_DIR/venv"
    # Check if venv exists AND is complete (has pip)
    if [ ! -x "$VENV_DIR/bin/pip" ]; then
        # Remove broken/incomplete venv if exists
        [ -d "$VENV_DIR" ] && rm -rf "$VENV_DIR"
        log_info "Creating Python virtual environment..."
        python3 -m venv "$VENV_DIR" || {
            log_error "Failed to create virtual environment (is python3-venv installed?)"
            return 1
        }
    fi

    # Verify pip exists after venv creation
    if [ ! -x "$VENV_DIR/bin/pip" ]; then
        log_error "venv created but pip missing (install python3-venv package)"
        return 1
    fi

    # Install packages in venv
    log_info "Installing scapy and aioquic in venv..."
    "$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null || true
    "$VENV_DIR/bin/pip" install --quiet scapy aioquic 2>/dev/null || {
        # Try with --break-system-packages as fallback (shouldn't be needed in venv)
        "$VENV_DIR/bin/pip" install scapy aioquic || {
            log_error "Failed to install Python packages (scapy, aioquic)"
            return 1
        }
    }

    # Verify
    if "$VENV_DIR/bin/python" -c "import scapy; import aioquic" 2>/dev/null; then
        log_success "Python dependencies installed (scapy, aioquic)"
    else
        log_error "Python package verification failed"
        return 1
    fi
}

install_microsocks() {
    log_info "Installing microsocks for SOCKS5 proxy..."
    if [ -x "$INSTALL_DIR/bin/microsocks" ]; then
        log_success "microsocks already installed"
        return 0
    fi
    # Build dependencies
    command -v gcc &>/dev/null || install_package gcc
    command -v make &>/dev/null || install_package make
    local tmp_dir
    tmp_dir=$(mktemp -d)
    if ! curl -sL "https://github.com/${MICROSOCKS_REPO}/archive/refs/heads/master.tar.gz" -o "$tmp_dir/microsocks.tar.gz"; then
        log_error "Failed to download microsocks"
        rm -rf "$tmp_dir"
        return 1
    fi
    tar -xzf "$tmp_dir/microsocks.tar.gz" -C "$tmp_dir" 2>/dev/null || {
        log_error "Failed to extract microsocks"
        rm -rf "$tmp_dir"
        return 1
    }
    local src_dir
    src_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "microsocks*" | head -1)
    if [ -z "$src_dir" ]; then
        log_error "microsocks source directory not found"
        rm -rf "$tmp_dir"
        return 1
    fi
    if ! make -C "$src_dir" -j"$(nproc 2>/dev/null || echo 1)" 2>/dev/null; then
        log_error "Failed to compile microsocks"
        rm -rf "$tmp_dir"
        return 1
    fi
    mkdir -p "$INSTALL_DIR/bin"
    cp "$src_dir/microsocks" "$INSTALL_DIR/bin/microsocks"
    chmod 755 "$INSTALL_DIR/bin/microsocks"
    rm -rf "$tmp_dir"
    log_success "microsocks installed"
}

#───────────────────────────────────────────────────────────────────────
# Xray Installation (for GFK server - provides SOCKS5 on port 443)
#───────────────────────────────────────────────────────────────────────

XRAY_CONFIG_DIR="/usr/local/etc/xray"
XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json"

check_xray_installed() {
    command -v xray &>/dev/null && return 0
    [ -x /usr/local/bin/xray ] && return 0
    [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && return 0
    return 1
}

install_xray() {
    if check_xray_installed; then
        log_info "Xray is already installed"
        return 0
    fi

    log_info "Installing Xray ${XRAY_VERSION_PINNED}..."

    # Use official Xray install script with pinned version for stability
    local tmp_script
    tmp_script=$(mktemp)
    if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then
        log_error "Failed to download Xray installer"
        rm -f "$tmp_script"
        return 1
    fi

    # Install specific version (not latest) for stability
    if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then
        log_error "Failed to install Xray"
        rm -f "$tmp_script"
        return 1
    fi
    rm -f "$tmp_script"

    log_success "Xray ${XRAY_VERSION_PINNED} installed"
}

configure_xray_socks() {
    local listen_port="${1:-443}"

    log_info "Configuring Xray SOCKS5 proxy on port $listen_port..."

    mkdir -p "$XRAY_CONFIG_DIR"

    # Create simple SOCKS5 inbound config
    cat > "$XRAY_CONFIG_FILE" << EOF
{
  "log": {
    "loglevel": "warning"
  },
  "inbounds": [
    {
      "tag": "socks-in",
      "port": ${listen_port},
      "listen": "127.0.0.1",
      "protocol": "socks",
      "settings": {
        "auth": "noauth",
        "udp": true
      },
      "sniffing": {
        "enabled": true,
        "destOverride": ["http", "tls"]
      }
    }
  ],
  "outbounds": [
    {
      "tag": "direct",
      "protocol": "freedom",
      "settings": {}
    }
  ]
}
EOF
    chmod 644 "$XRAY_CONFIG_FILE"  # Xray service runs as 'nobody', needs read access
    log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)"
}

# Check if running xray is paqctl's own standalone install (not a real panel)
# Returns 0 if standalone (all inbounds are socks on 127.0.0.1), 1 if panel
_is_paqctl_standalone_xray() {
    [ -f "$XRAY_CONFIG_FILE" ] || return 1
    command -v python3 &>/dev/null || return 1
    python3 -c "
import json, sys
try:
    with open(sys.argv[1]) as f:
        cfg = json.load(f)
    inbounds = cfg.get('inbounds', [])
    if not inbounds:
        sys.exit(1)
    for i in inbounds:
        if i.get('protocol') != 'socks' or i.get('listen', '0.0.0.0') != '127.0.0.1':
            sys.exit(1)
    sys.exit(0)
except:
    sys.exit(1)
" "$XRAY_CONFIG_FILE" 2>/dev/null
}

# Add a SOCKS5 inbound to an existing xray config (panel) without touching other inbounds
_add_xray_gfk_socks() {
    local port="$1"
    python3 -c "
import json, sys
port = int(sys.argv[1])
config_path = sys.argv[2]
try:
    with open(config_path, 'r') as f:
        cfg = json.load(f)
except:
    cfg = {'inbounds': [], 'outbounds': [{'tag': 'direct', 'protocol': 'freedom', 'settings': {}}]}
cfg.setdefault('inbounds', [])
cfg['inbounds'] = [i for i in cfg['inbounds'] if i.get('tag') != 'gfk-socks']
cfg['inbounds'].append({
    'tag': 'gfk-socks',
    'port': port,
    'listen': '127.0.0.1',
    'protocol': 'socks',
    'settings': {'auth': 'noauth', 'udp': True},
    'sniffing': {'enabled': True, 'destOverride': ['http', 'tls']}
})
if not any(o.get('protocol') == 'freedom' for o in cfg.get('outbounds', [])):
    cfg.setdefault('outbounds', []).append({'tag': 'direct', 'protocol': 'freedom', 'settings': {}})
with open(config_path, 'w') as f:
    json.dump(cfg, f, indent=2)
" "$port" "$XRAY_CONFIG_FILE" 2>/dev/null
    if [ $? -ne 0 ]; then
        log_error "Failed to add SOCKS5 inbound to existing Xray config"
        return 1
    fi
    log_success "Added GFK SOCKS5 inbound on 127.0.0.1:$port"
}

start_xray() {
    log_info "Starting Xray service..."

    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        # Stop first, reload daemon, then start - with retry
        systemctl stop xray 2>/dev/null || true
        sleep 1
        systemctl daemon-reload 2>/dev/null || true
        systemctl enable xray 2>/dev/null || true

        # Try up to 3 times
        local attempt
        for attempt in 1 2 3; do
            systemctl start xray 2>/dev/null
            sleep 2
            if systemctl is-active --quiet xray; then
                log_success "Xray started"
                return 0
            fi
            [ "$attempt" -lt 3 ] && sleep 1
        done
        log_error "Failed to start Xray after 3 attempts"
        return 1
    else
        # Direct start for non-systemd
        local _xray_bin=""
        [ -x /usr/local/bin/xray ] && _xray_bin="/usr/local/bin/xray"
        [ -z "$_xray_bin" ] && [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && _xray_bin="/usr/local/x-ui/bin/xray-linux-amd64"
        if [ -n "$_xray_bin" ]; then
            pkill -x xray 2>/dev/null || true
            sleep 1
            nohup "$_xray_bin" run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
            sleep 2
            if pgrep -f "xray" &>/dev/null; then
                log_success "Xray started"
                return 0
            fi
        fi
        log_error "Failed to start Xray"
        return 1
    fi
}

stop_xray() {
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl stop xray 2>/dev/null || true
    else
        pkill -x xray 2>/dev/null || true
    fi
}

setup_xray_for_gfk() {
    local target_port
    target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)

    if pgrep -x xray &>/dev/null || pgrep -f "xray-linux" &>/dev/null; then
        # Check if this is paqctl's own standalone Xray (not a real panel)
        if _is_paqctl_standalone_xray; then
            log_info "Existing Xray is paqctl's standalone install — reconfiguring..."
            stop_xray
            sleep 1
            # Fall through to standalone install path below
        else
            XRAY_PANEL_DETECTED=true
            log_info "Existing Xray detected — adding SOCKS5 alongside panel..."

            # Clean up any leftover standalone GFK xray from prior installs
            pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true
            rm -f "${XRAY_CONFIG_DIR}/gfk-socks.json" 2>/dev/null

            # Check all existing target ports from mappings
            local mapping pairs
            IFS=',' read -ra pairs <<< "${GFK_PORT_MAPPINGS:-14000:443}"
            for mapping in "${pairs[@]}"; do
                local vio_port="${mapping%%:*}"
                local tp="${mapping##*:}"
                if ss -tln 2>/dev/null | grep -q ":${tp} "; then
                    log_success "Port $tp is listening — GFK will forward VIO port $vio_port to this port"
                else
                    log_warn "Port $tp is NOT listening — make sure your panel inbound is on port $tp"
                fi
            done

            # Find free port for SOCKS5 (starting at 10443)
            local socks_port=10443
            while ss -tln 2>/dev/null | grep -q ":${socks_port} "; do
                socks_port=$((socks_port + 1))
                if [ "$socks_port" -gt 65000 ]; then
                    log_warn "Could not find free port for SOCKS5 — panel-only mode"
                    echo ""
                    local first_vio
                    first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
                    log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
                    return 0
                fi
            done

            # Add SOCKS5 inbound to existing xray config
            _add_xray_gfk_socks "$socks_port" || {
                log_warn "Could not add SOCKS5 to panel config — panel-only mode"
                return 0
            }

            # Restart xray to load new config
            systemctl restart xray 2>/dev/null || pkill -SIGHUP xray 2>/dev/null || true
            sleep 2

            # Find next VIO port (highest existing + 1) and append SOCKS5 mapping
            local max_vio=0
            for mapping in "${pairs[@]}"; do
                local v="${mapping%%:*}"
                [ "$v" -gt "$max_vio" ] && max_vio="$v"
            done
            local socks_vio=$((max_vio + 1))
            GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS},${socks_vio}:${socks_port}"
            GFK_SOCKS_PORT="$socks_port"
            GFK_SOCKS_VIO_PORT="$socks_vio"

            log_success "SOCKS5 proxy added on port $socks_port (VIO port $socks_vio)"
            echo ""
            log_info "Port mappings updated: ${GFK_PORT_MAPPINGS}"
            log_warn "Use these SAME mappings on the client side"
            echo ""
            local first_vio
            first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
            log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
            log_warn "For direct SOCKS5: use 127.0.0.1:${socks_vio} as your proxy on client"
            return 0
        fi
    fi

    install_xray || return 1
    configure_xray_socks "$target_port" || return 1
    start_xray || return 1
}

download_gfk() {
    log_info "Downloading GFW-knocker scripts..."
    if ! mkdir -p "$GFK_DIR"; then
        log_error "Failed to create $GFK_DIR"
        return 1
    fi
    # Note: parameters.py is generated by generate_gfk_config(), don't download it
    # Download server scripts from gfk/server/
    local server_files="mainserver.py quic_server.py vio_server.py"
    local f
    for f in $server_files; do
        if ! curl -sL "$GFK_RAW_URL/server/$f" -o "$GFK_DIR/$f"; then
            log_error "Failed to download $f"
            return 1
        fi
    done
    # Download client scripts from gfk/client/
    local client_files="mainclient.py quic_client.py vio_client.py"
    for f in $client_files; do
        if ! curl -sL "$GFK_RAW_URL/client/$f" -o "$GFK_DIR/$f"; then
            log_error "Failed to download $f"
            return 1
        fi
    done
    chmod 600 "$GFK_DIR"/*.py
    # Patch mainserver.py to use venv python for subprocesses
    if [ -f "$GFK_DIR/mainserver.py" ]; then
        sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py"
    fi
    log_success "GFW-knocker scripts downloaded to $GFK_DIR"
}

generate_gfk_certs() {
    if [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ]; then
        log_info "GFW-knocker certificates already exist"
        return 0
    fi
    if ! command -v openssl &>/dev/null; then
        log_info "Installing openssl..."
        install_package openssl || { log_error "Failed to install openssl"; return 1; }
    fi
    log_info "Generating QUIC TLS certificates..."
    if ! openssl req -x509 -newkey rsa:2048 -keyout "$GFK_DIR/key.pem" \
        -out "$GFK_DIR/cert.pem" -days 3650 -nodes -subj "/CN=gfk" 2>/dev/null; then
        log_error "Failed to generate certificates"
        return 1
    fi
    chmod 600 "$GFK_DIR/key.pem" "$GFK_DIR/cert.pem"
    log_success "QUIC certificates generated"
}

generate_gfk_config() {
    log_info "Generating GFW-knocker configuration..."
    # Ensure GFK directory exists
    mkdir -p "$GFK_DIR" || { log_error "Failed to create $GFK_DIR"; return 1; }
    local _tmp
    _tmp=$(mktemp "$GFK_DIR/parameters.py.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }

    # Determine port values based on role - validate they are numeric
    local vio_tcp_server_port="${GFK_VIO_PORT:-45000}"
    local vio_tcp_client_port="${GFK_VIO_CLIENT_PORT:-40000}"
    local vio_udp_server_port="${GFK_VIO_UDP_SERVER:-35000}"
    local vio_udp_client_port="${GFK_VIO_UDP_CLIENT:-30000}"
    local quic_server_port="${GFK_QUIC_PORT:-25000}"
    local quic_client_port="${GFK_QUIC_CLIENT_PORT:-20000}"

    # Validate all ports are numeric
    for _p in "$vio_tcp_server_port" "$vio_tcp_client_port" "$vio_udp_server_port" \
              "$vio_udp_client_port" "$quic_server_port" "$quic_client_port"; do
        if ! [[ "$_p" =~ ^[0-9]+$ ]]; then
            log_error "Invalid port number: $_p"
            rm -f "$_tmp"
            return 1
        fi
    done

    # Escape Python string - prevents code injection
    _escape_py_string() {
        local s="$1"
        s="${s//\\/\\\\}"   # Escape backslashes first
        s="${s//\"/\\\"}"   # Escape double quotes
        s="${s//\'/\\\'}"   # Escape single quotes
        s="${s//$'\n'/\\n}" # Escape newlines
        s="${s//$'\r'/\\r}" # Escape carriage returns
        printf '%s' "$s"
    }

    # Validate and escape server IP
    local safe_server_ip
    safe_server_ip=$(_escape_py_string "${GFK_SERVER_IP:-}")
    if ! _validate_ip "${GFK_SERVER_IP:-}"; then
        log_error "Invalid server IP: ${GFK_SERVER_IP:-}"
        rm -f "$_tmp"
        return 1
    fi

    # Validate and escape auth code
    local safe_auth_code
    safe_auth_code=$(_escape_py_string "${GFK_AUTH_CODE:-}")

    # Build port mapping dict string with validation
    local tcp_mapping="${GFK_PORT_MAPPINGS:-14000:443}"
    local mapping_str="{"
    local first=true
    local pair
    for pair in $(echo "$tcp_mapping" | tr ',' ' '); do
        local lport rport
        lport=$(echo "$pair" | cut -d: -f1)
        rport=$(echo "$pair" | cut -d: -f2)
        # Validate both ports are numeric
        if ! [[ "$lport" =~ ^[0-9]+$ ]] || ! [[ "$rport" =~ ^[0-9]+$ ]]; then
            log_error "Invalid port mapping: $pair (must be numeric:numeric)"
            rm -f "$_tmp"
            return 1
        fi
        if [ "$first" = true ]; then
            mapping_str="${mapping_str}${lport}: ${rport}"
            first=false
        else
            mapping_str="${mapping_str}, ${lport}: ${rport}"
        fi
    done
    mapping_str="${mapping_str}}"

    # Escape GFK_DIR for Python string
    local safe_gfk_dir
    safe_gfk_dir=$(_escape_py_string "${GFK_DIR}")

    (
    umask 077
    cat > "$_tmp" << PYEOF
# GFW-knocker parameters - auto-generated by paqctl
# Do not edit manually

vps_ip = "${safe_server_ip}"
xray_server_ip_address = "127.0.0.1"

tcp_port_mapping = ${mapping_str}
udp_port_mapping = {}

vio_tcp_server_port = ${vio_tcp_server_port}
vio_tcp_client_port = ${vio_tcp_client_port}
vio_udp_server_port = ${vio_udp_server_port}
vio_udp_client_port = ${vio_udp_client_port}

quic_server_port = ${quic_server_port}
quic_client_port = ${quic_client_port}
quic_local_ip = "127.0.0.1"

quic_idle_timeout = 86400
udp_timeout = 300
quic_mtu = 1420
quic_verify_cert = False
quic_max_data = 1073741824
quic_max_stream_data = 1073741824

quic_auth_code = "${safe_auth_code}"

quic_cert_filepath = ("${safe_gfk_dir}/cert.pem", "${safe_gfk_dir}/key.pem")

tcp_flags = "${GFK_TCP_FLAGS:-AP}"
PYEOF
    )
    if ! mv "$_tmp" "$GFK_DIR/parameters.py"; then
        log_error "Failed to save GFW-knocker configuration"
        rm -f "$_tmp"
        return 1
    fi
    chmod 600 "$GFK_DIR/parameters.py"
    log_success "GFW-knocker configuration saved"
}

create_gfk_client_wrapper() {
    log_info "Creating GFW-knocker client wrapper..."
    local wrapper="$INSTALL_DIR/bin/gfk-client.sh"
    mkdir -p "$INSTALL_DIR/bin"
    cat > "$wrapper" << 'WRAPEOF'
#!/bin/bash
set -e
GFK_DIR="REPLACE_ME_GFK_DIR"
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"

cd "$GFK_DIR"
"$INSTALL_DIR/venv/bin/python" mainclient.py &
PID1=$!
trap "kill $PID1 2>/dev/null; wait" EXIT INT TERM
wait
WRAPEOF
    sed "s#REPLACE_ME_GFK_DIR#${GFK_DIR}#g; s#REPLACE_ME_INSTALL_DIR#${INSTALL_DIR}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper"
    chmod 755 "$wrapper"
    log_success "Client wrapper created at $wrapper"
}

#═══════════════════════════════════════════════════════════════════════
# Service Management
#═══════════════════════════════════════════════════════════════════════

setup_service() {
    log_info "Setting up auto-start on boot..."

    # Check which backends are installed
    local paqet_installed=false gfk_installed=false
    [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
    if [ "$ROLE" = "server" ]; then
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
    else
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
    fi

    # If both backends are installed, create a combined service
    local _both_installed=false
    [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ] && _both_installed=true

    # Compute ExecStart based on backend
    local _exec_start _working_dir _svc_desc _svc_type="simple"
    if [ "$_both_installed" = true ]; then
        _svc_desc="Paqet Combined Proxy Service (Paqet + GFK)"
        _working_dir="${INSTALL_DIR}"
        _svc_type="forking"
        # Create a wrapper script that starts both backends
        cat > "${INSTALL_DIR}/bin/start-both.sh" << BOTH_SCRIPT
#!/bin/bash
INSTALL_DIR="/opt/paqctl"
GFK_DIR="\${INSTALL_DIR}/gfk"
ROLE="${ROLE}"

# Source config for ports
[ -f "\${INSTALL_DIR}/settings.conf" ] && . "\${INSTALL_DIR}/settings.conf"

# Detect firewall backend
_use_firewalld=false
if command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running; then
    _use_firewalld=true
fi

# Apply firewall rules (server + client)
if [ "\$ROLE" = "server" ]; then
    port="\${LISTEN_PORT:-8443}"
    vio_port="\${GFK_VIO_PORT:-45000}"
    TAG="paqctl"
    if [ "\$_use_firewalld" = true ]; then
        # Paqet rules via firewalld
        firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "\$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        # GFK rules via firewalld
        firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        # IPv6 GFK
        firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --runtime-to-permanent 2>/dev/null || true
    else
        # Paqet rules via iptables
        modprobe iptable_raw 2>/dev/null || true
        modprobe iptable_mangle 2>/dev/null || true
        iptables -t raw -C PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
            iptables -t raw -A PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
        iptables -t raw -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
            iptables -t raw -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
        iptables -t mangle -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \\
            iptables -t mangle -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null
        # GFK rules via iptables
        modprobe iptable_raw 2>/dev/null || true
        iptables -t raw -C PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
            iptables -t raw -A PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
        iptables -t raw -C OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
            iptables -t raw -A OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
        iptables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
            iptables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null
        iptables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
            iptables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null
        if command -v ip6tables &>/dev/null; then
            ip6tables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
                ip6tables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
            ip6tables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
                ip6tables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
        fi
    fi
else
    # GFK client firewall rules
    vio_client_port="\${GFK_VIO_CLIENT_PORT:-40000}"
    TAG="paqctl"
    if [ "\$_use_firewalld" = true ]; then
        firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --runtime-to-permanent 2>/dev/null || true
    else
        modprobe iptable_raw 2>/dev/null || true
        iptables -t raw -C PREROUTING -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
            iptables -t raw -A PREROUTING -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
        iptables -t raw -C OUTPUT -p tcp --sport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
            iptables -t raw -A OUTPUT -p tcp --sport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
        iptables -C INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
            iptables -A INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null
        iptables -C OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
            iptables -A OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null
        if command -v ip6tables &>/dev/null; then
            ip6tables -C INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
                ip6tables -A INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
            ip6tables -C OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
                ip6tables -A OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
        fi
    fi
fi

# Start paqet backend
(umask 077; touch /var/log/paqet-backend.log)
nohup "\${INSTALL_DIR}/bin/paqet" run -c "\${INSTALL_DIR}/config.yaml" > /var/log/paqet-backend.log 2>&1 &
echo \$! > /run/paqet-backend.pid

# Start GFK backend
(umask 077; touch /var/log/gfk-backend.log)
if [ "\$ROLE" = "server" ]; then
    # Start Xray if available
    if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ] || [ -x /usr/local/x-ui/bin/xray-linux-amd64 ]; then
        if ! pgrep -f "xray run" &>/dev/null; then
            systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null &
            sleep 2
        fi
    fi
    cd "\$GFK_DIR"
    nohup "\${INSTALL_DIR}/venv/bin/python" "\${GFK_DIR}/mainserver.py" > /var/log/gfk-backend.log 2>&1 &
else
    if [ -x "\${INSTALL_DIR}/bin/gfk-client.sh" ]; then
        nohup "\${INSTALL_DIR}/bin/gfk-client.sh" > /var/log/gfk-backend.log 2>&1 &
    else
        cd "\$GFK_DIR"
        nohup "\${INSTALL_DIR}/venv/bin/python" "\${GFK_DIR}/mainclient.py" > /var/log/gfk-backend.log 2>&1 &
    fi
fi
echo \$! > /run/gfk-backend.pid

sleep 1
exit 0
BOTH_SCRIPT
        chmod +x "${INSTALL_DIR}/bin/start-both.sh"
        _exec_start="${INSTALL_DIR}/bin/start-both.sh"
    elif [ "$BACKEND" = "gfw-knocker" ]; then
        _svc_desc="GFW-knocker Proxy Service"
        _working_dir="${GFK_DIR}"
        if [ "$ROLE" = "server" ]; then
            _exec_start="${INSTALL_DIR}/venv/bin/python ${GFK_DIR}/mainserver.py"
        else
            _exec_start="${INSTALL_DIR}/bin/gfk-client.sh"
        fi
    else
        _svc_desc="Paqet Proxy Service"
        _working_dir="${INSTALL_DIR}"
        _exec_start="${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml"
    fi

    if [ "$HAS_SYSTEMD" = "true" ]; then
        if [ "$_both_installed" = true ]; then
            # Combined service for both backends
            cat > /etc/systemd/system/paqctl.service << EOF
[Unit]
Description=${_svc_desc}
After=network-online.target
Wants=network-online.target

[Service]
Type=${_svc_type}
WorkingDirectory=${_working_dir}
ExecStart=${_exec_start}
ExecStop=/usr/local/bin/paqctl stop
ExecStopPost=/usr/local/bin/paqctl _remove-firewall
RemainAfterExit=yes
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30
LimitNOFILE=65535
StandardOutput=journal
StandardError=journal
SyslogIdentifier=paqctl

[Install]
WantedBy=multi-user.target
EOF
        else
            # Single backend service
            cat > /etc/systemd/system/paqctl.service << EOF
[Unit]
Description=${_svc_desc}
After=network-online.target
Wants=network-online.target

[Service]
Type=${_svc_type}
WorkingDirectory=${_working_dir}
ExecStartPre=/usr/local/bin/paqctl _apply-firewall
ExecStart=${_exec_start}
ExecStopPost=/usr/local/bin/paqctl _remove-firewall
Restart=on-failure
RestartSec=5
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30
LimitNOFILE=65535
StandardOutput=journal
StandardError=journal
SyslogIdentifier=paqctl

[Install]
WantedBy=multi-user.target
EOF
        fi

        systemctl daemon-reload 2>/dev/null || true
        systemctl enable paqctl.service 2>/dev/null || true
        log_success "Systemd service created and enabled"

    elif command -v rc-update &>/dev/null; then
        local _openrc_run
        _openrc_run=$(command -v openrc-run 2>/dev/null || echo "/sbin/openrc-run")
        cat > /etc/init.d/paqctl << EOF
#!${_openrc_run}

name="paqctl"
description="${_svc_desc}"
command="$(echo "${_exec_start}" | awk '{print $1}')"
command_args="$(echo "${_exec_start}" | cut -d' ' -f2-)"
if [ "\$command_args" = "\$command" ]; then command_args=""; fi
command_background=true
pidfile="/run/\${RC_SVCNAME}.pid"

depend() {
    need net
    after firewall
}

start_pre() {
    /usr/local/bin/paqctl _apply-firewall
}

stop_post() {
    /usr/local/bin/paqctl _remove-firewall
}
EOF
        if ! chmod +x /etc/init.d/paqctl; then
            log_error "Failed to make init script executable"
            return 1
        fi
        rc-update add paqctl default 2>/dev/null || true
        log_success "OpenRC service created and enabled"

    elif [ -d /etc/init.d ]; then
        cat > /etc/init.d/paqctl << SYSV
#!/bin/bash
### BEGIN INIT INFO
# Provides:          paqctl
# Required-Start:    \$remote_fs \$network \$syslog
# Required-Stop:     \$remote_fs \$network \$syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: ${_svc_desc}
### END INIT INFO

case "\$1" in
    start)
        /usr/local/bin/paqctl _apply-firewall
        ${_exec_start} &
        _pid=\$!
        sleep 1
        if kill -0 "\$_pid" 2>/dev/null; then
            echo \$_pid > /run/paqctl.pid
        else
            echo "Failed to start paqet"
            /usr/local/bin/paqctl _remove-firewall
            exit 1
        fi
        ;;
    stop)
        if [ -f /run/paqctl.pid ]; then
            _pid=\$(cat /run/paqctl.pid)
            kill "\$_pid" 2>/dev/null
            _count=0
            while kill -0 "\$_pid" 2>/dev/null && [ \$_count -lt 10 ]; do
                sleep 1
                _count=\$((_count + 1))
            done
            kill -0 "\$_pid" 2>/dev/null && kill -9 "\$_pid" 2>/dev/null
            rm -f /run/paqctl.pid
        fi
        /usr/local/bin/paqctl _remove-firewall
        ;;
    restart)
        \$0 stop
        sleep 1
        \$0 start
        ;;
    status)
        [ -f /run/paqctl.pid ] && kill -0 "\$(cat /run/paqctl.pid)" 2>/dev/null && echo "Running" || echo "Stopped"
        ;;
    *)
        echo "Usage: \$0 {start|stop|restart|status}"
        exit 1
        ;;
esac
SYSV
        if ! chmod +x /etc/init.d/paqctl; then
            log_error "Failed to make init script executable"
            return 1
        fi
        if command -v update-rc.d &>/dev/null; then
            update-rc.d paqctl defaults 2>/dev/null || true
        elif command -v chkconfig &>/dev/null; then
            chkconfig paqctl on 2>/dev/null || true
        fi
        log_success "SysVinit service created and enabled"

    else
        log_warn "Could not set up auto-start. You can start paqet manually with: sudo paqctl start"
    fi
}

setup_logrotate() {
    # Only set up if logrotate is available
    command -v logrotate &>/dev/null || return 0

    log_info "Setting up log rotation..."

    cat > /etc/logrotate.d/paqctl << 'LOGROTATE'
/var/log/paqctl.log
/var/log/paqet-backend.log
/var/log/gfk-backend.log
/var/log/xray.log
{
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0640 root root
    sharedscripts
    postrotate
        # Signal processes to reopen logs if needed
        systemctl reload paqctl.service 2>/dev/null || true
    endscript
}
LOGROTATE

    log_success "Log rotation configured (7 days, compressed)"
}

#═══════════════════════════════════════════════════════════════════════
# Management Script (Embedded)
#═══════════════════════════════════════════════════════════════════════

create_management_script() {
    local tmp_script
    tmp_script=$(mktemp "$INSTALL_DIR/paqctl.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
    cat > "$tmp_script" << 'MANAGEMENT'
#!/bin/bash
#
# paqctl - Paqet Manager
# https://git.samnet.dev/SamNet-dev/paqctl
#

VERSION="1.0.0"

# Pinned versions for stability (update these after testing new releases)
PAQET_VERSION_PINNED="v1.0.0-alpha.17"
XRAY_VERSION_PINNED="v26.2.4"
GFK_VERSION_PINNED="v1.0.0"

INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
BACKUP_DIR="$INSTALL_DIR/backups"
PAQET_REPO="hanselime/paqet"
PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest"
GFK_REPO="SamNet-dev/paqctl"
GFK_BRANCH="main"
GFK_RAW_URL="https://git.samnet.dev/${GFK_REPO}/raw/branch/${GFK_BRANCH}/gfk"
GFK_DIR="$INSTALL_DIR/gfk"
MICROSOCKS_REPO="rofl0r/microsocks"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# Input validation helpers
_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }
_validate_ip() {
    [[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
    local IFS='.'; set -- $1
    [ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ]
}
_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; }
_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; }
# Safe string length check - prevents DoS via extremely long inputs
_check_length() { [ ${#1} -le "${2:-256}" ]; }

# Network auto-detection
detect_network() {
    log_info "Auto-detecting network configuration..."

    # Default interface - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats
    # Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0
    # OpenVZ:   "default dev venet0 scope link"   -> $3 = venet0
    local _route_line
    _route_line=$(ip route show default 2>/dev/null | head -1)
    if [[ "$_route_line" == *" via "* ]]; then
        # Standard format with gateway
        DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}')
    elif [[ "$_route_line" == *" dev "* ]]; then
        # OpenVZ/direct format without gateway
        DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}')
    fi

    # Validate detected interface exists
    if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then
        DETECTED_IFACE=""
    fi

    if [ -z "$DETECTED_IFACE" ]; then
        # Note: grep -v returns exit 1 if no matches, so we add || true for pipefail
        DETECTED_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '{gsub(/ /,"",$2); print $2}' | { grep -vE '^(lo|docker[0-9]|br-|veth|virbr|tun|tap|wg)' || true; } | head -1)
    fi

    # Local IP - wrap entire pipeline to prevent pipefail exit
    if [ -n "$DETECTED_IFACE" ]; then
        # Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP
        DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true )
    fi
    if [ -z "$DETECTED_IP" ]; then
        DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
        [ -z "$DETECTED_IP" ] && DETECTED_IP=$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}')
    fi

    # Gateway IP - handle OpenVZ format (may not have gateway)
    if [[ "$_route_line" == *" via "* ]]; then
        DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}')
    else
        DETECTED_GATEWAY=""
    fi

    # Gateway MAC
    DETECTED_GW_MAC=""
    if [ -n "$DETECTED_GATEWAY" ]; then
        DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
        if [ -z "$DETECTED_GW_MAC" ]; then
            ping -c 1 -W 2 "$DETECTED_GATEWAY" &>/dev/null || true
            sleep 1
            DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
        fi
        if [ -z "$DETECTED_GW_MAC" ] && command -v arp &>/dev/null; then
            # Note: grep returns exit 1 if no matches, so we add || true for pipefail
            DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | head -1)
        fi
    fi

    log_info "Interface: ${DETECTED_IFACE:-unknown}"
    log_info "Local IP:  ${DETECTED_IP:-unknown}"
    log_info "Gateway:   ${DETECTED_GATEWAY:-unknown}"
    log_info "GW MAC:    ${DETECTED_GW_MAC:-unknown}"
}

_load_settings() {
    [ -f "$INSTALL_DIR/settings.conf" ] || return 0
    # Safe settings loading without eval - uses case statement
    while IFS='=' read -r key value; do
        [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
        value="${value#\"}"; value="${value%\"}"
        # Skip values with dangerous shell characters
        [[ "$value" =~ [\`\$\(] ]] && continue
        case "$key" in
            BACKEND) BACKEND="$value" ;;
            ROLE) ROLE="$value" ;;
            PAQET_VERSION) PAQET_VERSION="$value" ;;
            PAQCTL_VERSION) PAQCTL_VERSION="$value" ;;
            LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;;
            SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;;
            INTERFACE) INTERFACE="$value" ;;
            LOCAL_IP) LOCAL_IP="$value" ;;
            GATEWAY_MAC) GATEWAY_MAC="$value" ;;
            ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;;
            PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;;
            PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$value" ;;
            REMOTE_SERVER) REMOTE_SERVER="$value" ;;
            GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;;
            GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;;
            GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;;
            GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;;
            GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;;
            GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;;
            GFK_SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_PORT="$value" ;;
            GFK_SOCKS_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_VIO_PORT="$value" ;;
            XRAY_PANEL_DETECTED) XRAY_PANEL_DETECTED="$value" ;;
            MICROSOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && MICROSOCKS_PORT="$value" ;;
            GFK_SERVER_IP) GFK_SERVER_IP="$value" ;;
            GFK_TCP_FLAGS) [[ "$value" =~ ^[FSRPAUEC]+$ ]] && GFK_TCP_FLAGS="$value" ;;
            TELEGRAM_BOT_TOKEN) TELEGRAM_BOT_TOKEN="$value" ;;
            TELEGRAM_CHAT_ID) TELEGRAM_CHAT_ID="$value" ;;
            TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_INTERVAL="$value" ;;
            TELEGRAM_ENABLED) TELEGRAM_ENABLED="$value" ;;
            TELEGRAM_ALERTS_ENABLED) TELEGRAM_ALERTS_ENABLED="$value" ;;
            TELEGRAM_DAILY_SUMMARY) TELEGRAM_DAILY_SUMMARY="$value" ;;
            TELEGRAM_WEEKLY_SUMMARY) TELEGRAM_WEEKLY_SUMMARY="$value" ;;
            TELEGRAM_SERVER_LABEL) TELEGRAM_SERVER_LABEL="$value" ;;
            TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_START_HOUR="$value" ;;
        esac
    done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
}

# Load settings
_load_settings
ROLE=${ROLE:-server}
PAQET_VERSION=${PAQET_VERSION:-unknown}
LISTEN_PORT=${LISTEN_PORT:-8443}
SOCKS_PORT=${SOCKS_PORT:-1080}
INTERFACE=${INTERFACE:-eth0}
LOCAL_IP=${LOCAL_IP:-}
GATEWAY_MAC=${GATEWAY_MAC:-}
ENCRYPTION_KEY=${ENCRYPTION_KEY:-}
REMOTE_SERVER=${REMOTE_SERVER:-}
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
TELEGRAM_INTERVAL=${TELEGRAM_INTERVAL:-6}
TELEGRAM_ENABLED=${TELEGRAM_ENABLED:-false}
TELEGRAM_ALERTS_ENABLED=${TELEGRAM_ALERTS_ENABLED:-true}
TELEGRAM_DAILY_SUMMARY=${TELEGRAM_DAILY_SUMMARY:-true}
TELEGRAM_WEEKLY_SUMMARY=${TELEGRAM_WEEKLY_SUMMARY:-true}
TELEGRAM_SERVER_LABEL=${TELEGRAM_SERVER_LABEL:-}
TELEGRAM_START_HOUR=${TELEGRAM_START_HOUR:-0}
BACKEND=${BACKEND:-paqet}
GFK_VIO_PORT=${GFK_VIO_PORT:-}
GFK_QUIC_PORT=${GFK_QUIC_PORT:-}
GFK_AUTH_CODE=${GFK_AUTH_CODE:-}
GFK_PORT_MAPPINGS=${GFK_PORT_MAPPINGS:-}
GFK_SOCKS_PORT=${GFK_SOCKS_PORT:-}
GFK_SOCKS_VIO_PORT=${GFK_SOCKS_VIO_PORT:-}
XRAY_PANEL_DETECTED=${XRAY_PANEL_DETECTED:-false}
MICROSOCKS_PORT=${MICROSOCKS_PORT:-}
GFK_SERVER_IP=${GFK_SERVER_IP:-}
GFK_TCP_FLAGS=${GFK_TCP_FLAGS:-AP}

# Ensure root
if [ "$EUID" -ne 0 ]; then
    echo -e "${RED}Error: This command must be run as root (use sudo paqctl)${NC}"
    exit 1
fi

log_info()    { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[✓]${NC} $1"; }
log_warn()    { echo -e "${YELLOW}[!]${NC} $1"; }
log_error()   { echo -e "${RED}[✗]${NC} $1"; }

# Retry helper with exponential backoff for API requests
_curl_with_retry() {
    local url="$1"
    local max_attempts="${2:-3}"
    local attempt=1
    local delay=2
    local response=""
    while [ $attempt -le $max_attempts ]; do
        response=$(curl -s --max-time 15 "$url" 2>/dev/null)
        if [ -n "$response" ]; then
            if echo "$response" | grep -q '"message".*rate limit'; then
                log_warn "API rate limited, waiting ${delay}s..."
                sleep $delay
                delay=$((delay * 2))
                attempt=$((attempt + 1))
                continue
            fi
            echo "$response"
            return 0
        fi
        [ $attempt -lt $max_attempts ] && sleep $delay
        delay=$((delay * 2))
        attempt=$((attempt + 1))
    done
    return 1
}

_validate_version_tag() {
    # Strict validation: only allow vX.Y.Z or X.Y.Z format with optional -suffix
    [[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]
}

# Safe sed: escape replacement string to prevent metachar injection
_sed_escape() { printf '%s\n' "$1" | sed 's/[&/\]/\\&/g'; }
_safe_update_setting() {
    local key="$1" value="$2" file="$3"
    local escaped_value
    escaped_value=$(_sed_escape "$value")
    sed "s/^${key}=.*/${key}=\"${escaped_value}\"/" "$file" > "$file.tmp" 2>/dev/null && mv "$file.tmp" "$file" || true
}

print_header() {
    echo -e "${CYAN}"
    echo "╔════════════════════════════════════════════════════════════════╗"
    echo "║                  PAQCTL - Paqet Manager v${VERSION}                   ║"
    echo "║        Raw-socket encrypted proxy - bypass firewalls           ║"
    echo "╚════════════════════════════════════════════════════════════════╝"
    echo -e "${NC}"
}

#═══════════════════════════════════════════════════════════════════════
# Settings Save (management script)
#═══════════════════════════════════════════════════════════════════════

save_settings() {
    local _tg_token="${TELEGRAM_BOT_TOKEN:-}"
    local _tg_chat="${TELEGRAM_CHAT_ID:-}"
    local _tg_interval="${TELEGRAM_INTERVAL:-6}"
    local _tg_enabled="${TELEGRAM_ENABLED:-false}"
    local _tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}"
    local _tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}"
    local _tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}"
    local _tg_label="${TELEGRAM_SERVER_LABEL:-}"
    local _tg_start_hour="${TELEGRAM_START_HOUR:-0}"
    # Sanitize sensitive values - remove shell metacharacters and control chars
    _sanitize_value() {
        printf '%s' "$1" | tr -d '"$`\\'\''(){}[]<>|;&!\n\r\t'
    }
    local _safe_key; _safe_key=$(_sanitize_value "${ENCRYPTION_KEY:-}")
    local _safe_auth; _safe_auth=$(_sanitize_value "${GFK_AUTH_CODE:-}")
    local _tmp
    _tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
    (umask 077; cat > "$_tmp" << SEOF
BACKEND="${BACKEND:-paqet}"
ROLE="${ROLE}"
PAQET_VERSION="${PAQET_VERSION:-unknown}"
PAQCTL_VERSION="${VERSION}"
LISTEN_PORT="${LISTEN_PORT:-}"
SOCKS_PORT="${SOCKS_PORT:-}"
INTERFACE="${INTERFACE:-}"
LOCAL_IP="${LOCAL_IP:-}"
GATEWAY_MAC="${GATEWAY_MAC:-}"
ENCRYPTION_KEY="${_safe_key}"
PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}"
PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}"
REMOTE_SERVER="${REMOTE_SERVER:-}"
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
GFK_VIO_CLIENT_PORT="${GFK_VIO_CLIENT_PORT:-}"
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
GFK_QUIC_CLIENT_PORT="${GFK_QUIC_CLIENT_PORT:-}"
GFK_AUTH_CODE="${_safe_auth}"
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}"
GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}"
XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}"
MICROSOCKS_PORT="${MICROSOCKS_PORT:-}"
GFK_SERVER_IP="${GFK_SERVER_IP:-}"
GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}"
TELEGRAM_BOT_TOKEN="${_tg_token}"
TELEGRAM_CHAT_ID="${_tg_chat}"
TELEGRAM_INTERVAL=${_tg_interval}
TELEGRAM_ENABLED=${_tg_enabled}
TELEGRAM_ALERTS_ENABLED=${_tg_alerts}
TELEGRAM_DAILY_SUMMARY=${_tg_daily}
TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly}
TELEGRAM_SERVER_LABEL="${_tg_label}"
TELEGRAM_START_HOUR=${_tg_start_hour}
SEOF
    )
    if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then
        log_error "Failed to save settings"
        rm -f "$_tmp"
        return 1
    fi
    chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null
}

#═══════════════════════════════════════════════════════════════════════
# Architecture Detection & Paqet Download (management script)
#═══════════════════════════════════════════════════════════════════════

detect_arch() {
    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64) echo "amd64" ;;
        aarch64|arm64) echo "arm64" ;;
        armv7l|armv7|armhf) echo "arm32" ;;
        mips64el|mips64le) echo "mips64le" ;;
        mips64) echo "mips64" ;;
        mipsel|mipsle) echo "mipsle" ;;
        mips) echo "mips" ;;
        *)
            log_error "Unsupported architecture: $arch"
            return 1
            ;;
    esac
}

download_paqet() {
    local version="$1"
    local arch
    arch=$(detect_arch) || return 1
    local os_name="linux"
    local ext="tar.gz"
    local filename="paqet-${os_name}-${arch}-${version}.${ext}"
    local url="https://github.com/${PAQET_REPO}/releases/download/${version}/${filename}"

    log_info "Downloading paqet ${version} for ${os_name}/${arch}..."

    mkdir -p "$INSTALL_DIR/bin" || { log_error "Failed to create directory"; return 1; }
    local tmp_file
    tmp_file=$(mktemp "/tmp/paqet-download-XXXXXXXX.${ext}") || { log_error "Failed to create temp file"; return 1; }

    # Try curl first, fallback to wget
    local download_ok=false
    if curl -sL --max-time 180 --retry 3 --retry-delay 5 --fail -o "$tmp_file" "$url" 2>/dev/null; then
        download_ok=true
    elif command -v wget &>/dev/null; then
        log_info "curl failed, trying wget..."
        rm -f "$tmp_file"
        if wget -q --timeout=180 --tries=3 -O "$tmp_file" "$url" 2>/dev/null; then
            download_ok=true
        fi
    fi

    if [ "$download_ok" != "true" ]; then
        log_error "Failed to download: $url"
        log_error "Try manual download: wget '$url' and place binary in $INSTALL_DIR/bin/"
        rm -f "$tmp_file"
        return 1
    fi

    # Validate download
    local fsize
    fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
    if [ "$fsize" -lt 1000 ]; then
        log_error "Downloaded file is too small ($fsize bytes)"
        rm -f "$tmp_file"
        return 1
    fi

    # Extract
    log_info "Extracting..."
    local tmp_extract
    tmp_extract=$(mktemp -d "/tmp/paqet-extract-XXXXXXXX") || { log_error "Failed to create temp dir"; return 1; }
    if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
        log_error "Failed to extract archive"
        rm -f "$tmp_file"; rm -rf "$tmp_extract"
        return 1
    fi

    # Find the binary
    local binary_name="paqet_${os_name}_${arch}"
    local found_binary=""
    found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
    [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
    [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)

    if [ -z "$found_binary" ]; then
        log_error "Could not find paqet binary in archive"
        rm -f "$tmp_file"; rm -rf "$tmp_extract"
        return 1
    fi

    # Stop paqet if running to avoid "Text file busy" error
    pkill -f "$INSTALL_DIR/bin/paqet" 2>/dev/null || true
    sleep 1

    if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
        log_error "Failed to copy paqet binary"
        rm -f "$tmp_file"; rm -rf "$tmp_extract"
        return 1
    fi
    chmod +x "$INSTALL_DIR/bin/paqet" || { log_error "Failed to make paqet executable"; return 1; }

    rm -f "$tmp_file"; rm -rf "$tmp_extract"

    if "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
        log_success "paqet ${version} installed successfully"
    else
        log_warn "paqet installed but version check failed (may need libpcap)"
    fi
}

#═══════════════════════════════════════════════════════════════════════
# GFK Helper Functions (management script)
#═══════════════════════════════════════════════════════════════════════

install_python_deps() {
    log_info "Installing Python dependencies..."
    if ! command -v python3 &>/dev/null; then
        if command -v apt-get &>/dev/null; then apt-get install -y python3 2>/dev/null
        elif command -v dnf &>/dev/null; then dnf install -y python3 2>/dev/null
        elif command -v yum &>/dev/null; then yum install -y python3 2>/dev/null
        elif command -v apk &>/dev/null; then apk add python3 2>/dev/null
        fi
    fi
    # Verify Python 3.10+ (required for GFK)
    local pyver pymajor pyminor
    pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "0.0")
    pymajor=$(echo "$pyver" | cut -d. -f1)
    pyminor=$(echo "$pyver" | cut -d. -f2)
    if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then
        log_error "Python 3.10+ required, found $pyver"
        return 1
    fi
    # Install python3-venv (version-specific for apt, generic for others)
    if command -v apt-get &>/dev/null; then
        apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv 2>/dev/null
    elif command -v dnf &>/dev/null; then
        dnf install -y python3-pip 2>/dev/null  # dnf includes venv in python3
    elif command -v yum &>/dev/null; then
        yum install -y python3-pip 2>/dev/null
    elif command -v apk &>/dev/null; then
        apk add py3-pip 2>/dev/null
    fi
    # Use venv (recreate if broken/incomplete)
    local VENV_DIR="$INSTALL_DIR/venv"
    if [ ! -x "$VENV_DIR/bin/pip" ]; then
        [ -d "$VENV_DIR" ] && rm -rf "$VENV_DIR"
        python3 -m venv "$VENV_DIR" || { log_error "Failed to create venv (is python3-venv installed?)"; return 1; }
    fi
    # Verify pip exists after venv creation
    if [ ! -x "$VENV_DIR/bin/pip" ]; then
        log_error "venv created but pip missing (install python${pyver}-venv)"
        return 1
    fi
    "$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null || true
    "$VENV_DIR/bin/pip" install --quiet scapy aioquic 2>/dev/null || { log_error "Failed to install Python packages"; return 1; }
    "$VENV_DIR/bin/python" -c "import scapy; import aioquic" 2>/dev/null || { log_error "Python deps verification failed"; return 1; }
    log_success "Python dependencies OK"
}

install_microsocks() {
    log_info "Installing microsocks..."
    [ -x "$INSTALL_DIR/bin/microsocks" ] && { log_success "microsocks already installed"; return 0; }
    command -v gcc &>/dev/null || {
        if command -v apt-get &>/dev/null; then apt-get install -y gcc make 2>/dev/null
        elif command -v yum &>/dev/null; then yum install -y gcc make 2>/dev/null
        elif command -v apk &>/dev/null; then apk add gcc make musl-dev 2>/dev/null
        fi
    }
    local tmp_dir; tmp_dir=$(mktemp -d)
    curl -sL "https://github.com/${MICROSOCKS_REPO}/archive/refs/heads/master.tar.gz" -o "$tmp_dir/ms.tar.gz" || { rm -rf "$tmp_dir"; return 1; }
    tar -xzf "$tmp_dir/ms.tar.gz" -C "$tmp_dir" 2>/dev/null || { rm -rf "$tmp_dir"; return 1; }
    local src; src=$(find "$tmp_dir" -maxdepth 1 -type d -name "microsocks*" | head -1)
    [ -z "$src" ] && { rm -rf "$tmp_dir"; return 1; }
    make -C "$src" -j"$(nproc 2>/dev/null || echo 1)" 2>/dev/null || { rm -rf "$tmp_dir"; return 1; }
    mkdir -p "$INSTALL_DIR/bin"
    cp "$src/microsocks" "$INSTALL_DIR/bin/microsocks"
    chmod 755 "$INSTALL_DIR/bin/microsocks"
    rm -rf "$tmp_dir"
    log_success "microsocks installed"
}

download_gfk() {
    log_info "Downloading GFW-knocker scripts..."
    mkdir -p "$GFK_DIR" || return 1
    # Note: parameters.py is generated by generate_gfk_config(), don't download it
    local f
    # Download server scripts from gfk/server/
    for f in mainserver.py quic_server.py vio_server.py; do
        curl -sL "$GFK_RAW_URL/server/$f" -o "$GFK_DIR/$f" || { log_error "Failed to download $f"; return 1; }
    done
    # Download client scripts from gfk/client/
    for f in mainclient.py quic_client.py vio_client.py; do
        curl -sL "$GFK_RAW_URL/client/$f" -o "$GFK_DIR/$f" || { log_error "Failed to download $f"; return 1; }
    done
    chmod 600 "$GFK_DIR"/*.py
    # Patch mainserver.py to use venv python for subprocesses
    [ -f "$GFK_DIR/mainserver.py" ] && sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py"
    log_success "GFW-knocker scripts downloaded"
}

generate_gfk_certs() {
    [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ] && return 0
    if ! command -v openssl &>/dev/null; then
        log_info "Installing openssl..."
        if command -v apt-get &>/dev/null; then apt-get install -y openssl 2>/dev/null
        elif command -v dnf &>/dev/null; then dnf install -y openssl 2>/dev/null
        elif command -v yum &>/dev/null; then yum install -y openssl 2>/dev/null
        elif command -v apk &>/dev/null; then apk add openssl 2>/dev/null
        elif command -v pacman &>/dev/null; then pacman -S --noconfirm openssl 2>/dev/null
        fi
        command -v openssl &>/dev/null || { log_error "Failed to install openssl"; return 1; }
    fi
    log_info "Generating QUIC certificates..."
    openssl req -x509 -newkey rsa:2048 -keyout "$GFK_DIR/key.pem" \
        -out "$GFK_DIR/cert.pem" -days 3650 -nodes -subj "/CN=gfk" 2>/dev/null || return 1
    chmod 600 "$GFK_DIR/key.pem" "$GFK_DIR/cert.pem"
    log_success "QUIC certificates generated"
}

generate_gfk_config() {
    log_info "Generating GFW-knocker config..."
    local _tmp; _tmp=$(mktemp "$GFK_DIR/parameters.py.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
    local vio_tcp_server_port="${GFK_VIO_PORT:-45000}"
    local vio_tcp_client_port="${GFK_VIO_CLIENT_PORT:-40000}"
    local vio_udp_server_port="${GFK_VIO_UDP_SERVER:-35000}"
    local vio_udp_client_port="${GFK_VIO_UDP_CLIENT:-30000}"
    local quic_server_port="${GFK_QUIC_PORT:-25000}"
    local quic_client_port="${GFK_QUIC_CLIENT_PORT:-20000}"
    # Validate all ports are numeric
    local _p; for _p in "$vio_tcp_server_port" "$vio_tcp_client_port" "$vio_udp_server_port" \
              "$vio_udp_client_port" "$quic_server_port" "$quic_client_port"; do
        [[ "$_p" =~ ^[0-9]+$ ]] || { log_error "Invalid port: $_p"; rm -f "$_tmp"; return 1; }
    done
    # Escape Python strings to prevent code injection
    _esc_py() { local s="$1"; s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; s="${s//\'/\\\'}"; printf '%s' "$s"; }
    local safe_ip; safe_ip=$(_esc_py "${GFK_SERVER_IP:-}")
    local safe_auth; safe_auth=$(_esc_py "${GFK_AUTH_CODE:-}")
    local safe_dir; safe_dir=$(_esc_py "${GFK_DIR}")
    # Validate and build port mapping
    local tcp_mapping="${GFK_PORT_MAPPINGS:-14000:443}"
    local mapping_str="{" first=true pair lport rport
    for pair in $(echo "$tcp_mapping" | tr ',' ' '); do
        lport=$(echo "$pair" | cut -d: -f1); rport=$(echo "$pair" | cut -d: -f2)
        [[ "$lport" =~ ^[0-9]+$ ]] && [[ "$rport" =~ ^[0-9]+$ ]] || { log_error "Invalid mapping: $pair"; rm -f "$_tmp"; return 1; }
        [ "$first" = true ] && { mapping_str="${mapping_str}${lport}: ${rport}"; first=false; } || mapping_str="${mapping_str}, ${lport}: ${rport}"
    done
    mapping_str="${mapping_str}}"
    (umask 077; cat > "$_tmp" << PYEOF
vps_ip = "${safe_ip}"
xray_server_ip_address = "127.0.0.1"
tcp_port_mapping = ${mapping_str}
udp_port_mapping = {}
vio_tcp_server_port = ${vio_tcp_server_port}
vio_tcp_client_port = ${vio_tcp_client_port}
vio_udp_server_port = ${vio_udp_server_port}
vio_udp_client_port = ${vio_udp_client_port}
quic_server_port = ${quic_server_port}
quic_client_port = ${quic_client_port}
quic_local_ip = "127.0.0.1"
quic_idle_timeout = 86400
udp_timeout = 300
quic_mtu = 1420
quic_verify_cert = False
quic_max_data = 1073741824
quic_max_stream_data = 1073741824
quic_auth_code = "${safe_auth}"
quic_cert_filepath = ("${safe_dir}/cert.pem", "${safe_dir}/key.pem")
tcp_flags = "${GFK_TCP_FLAGS:-AP}"
PYEOF
    )
    mv "$_tmp" "$GFK_DIR/parameters.py" || { rm -f "$_tmp"; return 1; }
    chmod 600 "$GFK_DIR/parameters.py"
    log_success "GFW-knocker config saved"
}

create_gfk_client_wrapper() {
    local wrapper="$INSTALL_DIR/bin/gfk-client.sh"
    mkdir -p "$INSTALL_DIR/bin"
    cat > "$wrapper" << 'WEOF'
#!/bin/bash
set -e
GFK_DIR="REPLACE_GFK"
INSTALL_DIR="REPLACE_INST"
cd "$GFK_DIR"
"$INSTALL_DIR/venv/bin/python" mainclient.py &
PID1=$!
trap "kill $PID1 2>/dev/null; wait" EXIT INT TERM
wait
WEOF
    sed "s#REPLACE_GFK#${GFK_DIR}#g; s#REPLACE_INST#${INSTALL_DIR}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper"
    chmod 755 "$wrapper"
}

#═══════════════════════════════════════════════════════════════════════
# Service Control
#═══════════════════════════════════════════════════════════════════════

is_running() {
    # Check which backends are installed
    local paqet_installed=false gfk_installed=false
    [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
    if [ "$ROLE" = "server" ]; then
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
    else
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
    fi

    # If both backends installed, return true if EITHER is running
    if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
        is_paqet_running && return 0
        is_gfk_running && return 0
        return 1
    fi

    # Single backend mode - original logic
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl is-active paqctl.service &>/dev/null && return 0
    elif [ -f /run/paqctl.pid ]; then
        local _pid
        _pid=$(cat /run/paqctl.pid 2>/dev/null)
        # Validate PID is numeric and process exists
        [[ "$_pid" =~ ^[0-9]+$ ]] && kill -0 "$_pid" 2>/dev/null && return 0
    fi
    # Also check for the process directly with more specific patterns
    if [ "$BACKEND" = "gfw-knocker" ]; then
        # Use full path matching to avoid false positives
        pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0
        pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0
        pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0
    else
        # Match specific config file path
        pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0
    fi
    return 1
}

# Check if paqet backend specifically is running
is_paqet_running() {
    pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0
    return 1
}

# Check if GFK backend specifically is running
is_gfk_running() {
    if [ "$ROLE" = "server" ]; then
        pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0
    else
        pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0
        pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0
    fi
    return 1
}

# Start paqet backend only
start_paqet_backend() {
    if is_paqet_running; then
        log_warn "paqet is already running"
        return 0
    fi

    if [ ! -f "$INSTALL_DIR/bin/paqet" ]; then
        log_error "paqet binary not installed. Use 'Install additional backend' first."
        return 1
    fi

    # Generate config.yaml if missing - prompt for values
    if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
        echo ""
        echo -e "${YELLOW}config.yaml not found. Let's configure paqet:${NC}"
        echo ""

        detect_network
        local _det_iface="$DETECTED_IFACE"
        local _det_ip="$DETECTED_IP"
        local _det_mac="$DETECTED_GW_MAC"

        echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:"
        read -p "  Interface: " input < /dev/tty || true
        local _iface="${input:-${_det_iface:-eth0}}"

        echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:"
        read -p "  IP: " input < /dev/tty || true
        local _local_ip="${input:-$_det_ip}"

        echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:"
        read -p "  MAC: " input < /dev/tty || true
        local _gw_mac="${input:-$_det_mac}"

        local _key
        _key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
        if [ -z "$_key" ]; then
            _key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
        fi

        if [ "$ROLE" = "server" ]; then
            echo -e "${BOLD}Listen Port${NC} [8443]:"
            read -p "  Port: " input < /dev/tty || true
            local _port="${input:-8443}"

            echo ""
            echo -e "${GREEN}${BOLD}  Generated Key: ${_key}${NC}"
            echo -e "${BOLD}Encryption Key${NC} (Enter to use generated):"
            read -p "  Key: " input < /dev/tty || true
            [ -n "$input" ] && _key="$input"

            LISTEN_PORT="$_port"
            ENCRYPTION_KEY="$_key"

            cat > "$INSTALL_DIR/config.yaml" << EOFCFG
role: "server"
log:
  level: "info"
listen:
  addr: ":${_port}"
network:
  interface: "${_iface}"
  ipv4:
    addr: "${_local_ip}:${_port}"
    router_mac: "${_gw_mac}"
transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_key}"
EOFCFG
        else
            echo -e "${BOLD}Remote Server${NC} (IP:PORT):"
            read -p "  Server: " input < /dev/tty || true
            local _server="${input:-${REMOTE_SERVER:-}}"

            echo -e "${BOLD}Encryption Key${NC} (from server):"
            read -p "  Key: " input < /dev/tty || true
            [ -n "$input" ] && _key="$input"

            echo -e "${BOLD}SOCKS5 Port${NC} [1080]:"
            read -p "  Port: " input < /dev/tty || true
            local _socks="${input:-1080}"

            REMOTE_SERVER="$_server"
            SOCKS_PORT="$_socks"
            ENCRYPTION_KEY="$_key"

            cat > "$INSTALL_DIR/config.yaml" << EOFCFG
role: "client"
log:
  level: "info"
socks5:
  - listen: "127.0.0.1:${_socks}"
network:
  interface: "${_iface}"
  ipv4:
    addr: "${_local_ip}:0"
    router_mac: "${_gw_mac}"
server:
  addr: "${_server}"
transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_key}"
EOFCFG
        fi

        if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
            log_error "Failed to write config.yaml"
            return 1
        fi
        chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
        INTERFACE="$_iface"
        LOCAL_IP="$_local_ip"
        GATEWAY_MAC="$_gw_mac"
        save_settings 2>/dev/null || true
        log_success "Configuration saved"
        echo ""
    fi

    log_info "Starting paqet backend..."

    # Apply paqet firewall rules
    local _saved_backend="$BACKEND"
    BACKEND="paqet"
    _apply_firewall
    BACKEND="$_saved_backend"

    (umask 077; touch /var/log/paqet-backend.log)
    nohup "$INSTALL_DIR/bin/paqet" run -c "$INSTALL_DIR/config.yaml" > /var/log/paqet-backend.log 2>&1 &
    echo $! > /run/paqet-backend.pid

    sleep 2
    if is_paqet_running; then
        log_success "paqet backend started"
    else
        log_error "paqet failed to start. Check: tail /var/log/paqet-backend.log"
        return 1
    fi
}

# Stop paqet backend only
stop_paqet_backend() {
    if ! is_paqet_running; then
        log_warn "paqet is not running"
        return 0
    fi

    log_info "Stopping paqet backend..."

    if [ -f /run/paqet-backend.pid ]; then
        local _pid
        _pid=$(cat /run/paqet-backend.pid 2>/dev/null)
        if [ -n "$_pid" ] && [[ "$_pid" =~ ^[0-9]+$ ]]; then
            kill "$_pid" 2>/dev/null
            sleep 1
            kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null
        fi
        rm -f /run/paqet-backend.pid
    fi

    pkill -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true

    # Remove paqet firewall rules
    local _saved_backend="$BACKEND"
    BACKEND="paqet"
    _remove_firewall
    BACKEND="$_saved_backend"

    sleep 1
    if ! is_paqet_running; then
        log_success "paqet backend stopped"
    else
        pkill -9 -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
        log_success "paqet backend stopped (forced)"
    fi
}

# Start GFK backend only
start_gfk_backend() {
    if is_gfk_running; then
        log_warn "gfw-knocker is already running"
        return 0
    fi

    if [ ! -d "$GFK_DIR" ] || [ ! -f "$GFK_DIR/quic_server.py" ]; then
        log_error "gfw-knocker not installed. Use 'Install additional backend' first."
        return 1
    fi

    log_info "Starting gfw-knocker backend..."

    # Apply GFK firewall rules
    local _saved_backend="$BACKEND"
    BACKEND="gfw-knocker"
    _apply_firewall
    BACKEND="$_saved_backend"

    (umask 077; touch /var/log/gfk-backend.log)

    if [ "$ROLE" = "server" ]; then
        # Start Xray if not running
        if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ] || [ -x /usr/local/x-ui/bin/xray-linux-amd64 ]; then
            if ! pgrep -f "xray run" &>/dev/null; then
                systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null &
                sleep 2
            fi
        fi
        # Run from GFK_DIR so relative script paths work
        pushd "$GFK_DIR" >/dev/null
        nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainserver.py" > /var/log/gfk-backend.log 2>&1 &
        popd >/dev/null
    else
        if [ -x "$INSTALL_DIR/bin/gfk-client.sh" ]; then
            nohup "$INSTALL_DIR/bin/gfk-client.sh" > /var/log/gfk-backend.log 2>&1 &
        else
            # Run from GFK_DIR so relative script paths work
            pushd "$GFK_DIR" >/dev/null
            nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainclient.py" > /var/log/gfk-backend.log 2>&1 &
            popd >/dev/null
        fi
    fi
    echo $! > /run/gfk-backend.pid

    sleep 2
    if is_gfk_running; then
        log_success "gfw-knocker backend started"
    else
        log_error "gfw-knocker failed to start. Check: tail /var/log/gfk-backend.log"
        return 1
    fi
}

# Stop GFK backend only
stop_gfk_backend() {
    if ! is_gfk_running; then
        log_warn "gfw-knocker is not running"
        return 0
    fi

    log_info "Stopping gfw-knocker backend..."

    if [ -f /run/gfk-backend.pid ]; then
        local _pid
        _pid=$(cat /run/gfk-backend.pid 2>/dev/null)
        if [ -n "$_pid" ] && [[ "$_pid" =~ ^[0-9]+$ ]]; then
            kill "$_pid" 2>/dev/null
            sleep 1
            kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null
        fi
        rm -f /run/gfk-backend.pid
    fi

    pkill -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
    pkill -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
    pkill -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
    pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true

    # Remove GFK firewall rules
    local _saved_backend="$BACKEND"
    BACKEND="gfw-knocker"
    _remove_firewall
    BACKEND="$_saved_backend"

    sleep 1
    if ! is_gfk_running; then
        log_success "gfw-knocker backend stopped"
    else
        pkill -9 -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
        pkill -9 -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
        pkill -9 -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
        log_success "gfw-knocker backend stopped (forced)"
    fi
}

start_paqet() {
    # Check which backends are installed
    local paqet_installed=false gfk_installed=false
    [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
    if [ "$ROLE" = "server" ]; then
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
    else
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
    fi

    # If both backends installed, start both
    if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
        local started_something=false
        if ! is_paqet_running; then
            start_paqet_backend && started_something=true
        else
            log_warn "paqet is already running"
        fi
        if ! is_gfk_running; then
            start_gfk_backend && started_something=true
        else
            log_warn "gfw-knocker is already running"
        fi
        [ "$started_something" = true ] && return 0
        return 0
    fi

    # Single backend mode - original logic
    if is_running; then
        log_warn "${BACKEND} is already running"
        return 0
    fi

    log_info "Starting paqet..."
    local _direct_start=false

    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl start paqctl.service 2>/dev/null
    elif command -v rc-service &>/dev/null; then
        rc-service paqctl start 2>/dev/null
    elif [ -x /etc/init.d/paqctl ]; then
        /etc/init.d/paqctl start 2>/dev/null
    else
        # Direct start - track for cleanup on failure
        _direct_start=true
        _apply_firewall
        (umask 077; touch /var/log/paqctl.log)
        if [ "$BACKEND" = "gfw-knocker" ]; then
            if [ "$ROLE" = "client" ] && [ -x "$INSTALL_DIR/bin/gfk-client.sh" ]; then
                nohup "$INSTALL_DIR/bin/gfk-client.sh" > /var/log/paqctl.log 2>&1 &
            else
                nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainserver.py" > /var/log/paqctl.log 2>&1 &
            fi
        else
            nohup "$INSTALL_DIR/bin/paqet" run -c "$INSTALL_DIR/config.yaml" > /var/log/paqctl.log 2>&1 &
        fi
        echo $! > /run/paqctl.pid
    fi

    sleep 2
    if is_running; then
        log_success "${BACKEND} started successfully"
    else
        log_error "${BACKEND} failed to start. Check logs: sudo paqctl logs"
        # Clean up firewall rules on failure (only for direct start)
        if [ "$_direct_start" = true ]; then
            _remove_firewall
        fi
        return 1
    fi
}

stop_paqet() {
    # Check which backends are installed
    local paqet_installed=false gfk_installed=false
    [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
    if [ "$ROLE" = "server" ]; then
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
    else
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
    fi

    # If both backends installed, stop both
    if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
        local stopped_something=false
        if is_paqet_running; then
            stop_paqet_backend && stopped_something=true
        fi
        if is_gfk_running; then
            stop_gfk_backend && stopped_something=true
        fi
        if [ "$stopped_something" = false ]; then
            log_warn "No backends are running"
        fi
        return 0
    fi

    # Single backend mode - original logic
    if ! is_running; then
        log_warn "paqet is not running"
        return 0
    fi

    log_info "Stopping paqet..."

    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl stop paqctl.service 2>/dev/null
    elif command -v rc-service &>/dev/null; then
        rc-service paqctl stop 2>/dev/null
    elif [ -x /etc/init.d/paqctl ]; then
        /etc/init.d/paqctl stop 2>/dev/null
    else
        if [ -f /run/paqctl.pid ]; then
            local _pid
            _pid=$(cat /run/paqctl.pid 2>/dev/null)
            if [ -n "$_pid" ]; then
                kill "$_pid" 2>/dev/null
                local _count=0
                while kill -0 "$_pid" 2>/dev/null && [ $_count -lt 10 ]; do
                    sleep 1
                    _count=$((_count + 1))
                done
                kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null
            fi
            rm -f /run/paqctl.pid
        fi
        # Use specific paths to avoid killing unrelated processes
        if [ "$BACKEND" = "gfw-knocker" ]; then
            pkill -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
            pkill -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
            pkill -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
            pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true
        else
            pkill -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
        fi
        _remove_firewall
    fi

    sleep 1
    if ! is_running; then
        log_success "${BACKEND} stopped"
    else
        log_warn "${BACKEND} may still be running, force killing..."
        if [ "$BACKEND" = "gfw-knocker" ]; then
            pkill -9 -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
            pkill -9 -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
            pkill -9 -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
            pkill -9 -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true
        else
            pkill -9 -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
        fi
        sleep 1
        log_success "${BACKEND} stopped"
    fi
}

restart_paqet() {
    stop_paqet
    sleep 1
    start_paqet
}

#═══════════════════════════════════════════════════════════════════════
# Firewall (internal commands)
#═══════════════════════════════════════════════════════════════════════

_is_firewalld_active() {
    command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running
}

_apply_firewall() {
    if ! _is_firewalld_active && ! command -v iptables &>/dev/null; then
        echo -e "${YELLOW}[!]${NC} No firewall backend found (iptables or firewalld)." >&2
        return 1
    fi

    if [ "$BACKEND" = "gfw-knocker" ]; then
        local vio_port
        if [ "$ROLE" = "server" ]; then
            vio_port="${GFK_VIO_PORT:-45000}"
        else
            vio_port="${GFK_VIO_CLIENT_PORT:-40000}"
        fi

        if _is_firewalld_active; then
            firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
            firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
            firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                echo -e "${YELLOW}[!]${NC} Failed to add VIO port DROP rule via firewalld" >&2
            firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule via firewalld" >&2
            firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
            firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        else
            local TAG="paqctl"
            modprobe iptable_raw 2>/dev/null || true
            iptables -t raw -C PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
                iptables -t raw -A PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            iptables -t raw -C OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
                iptables -t raw -A OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            iptables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
                iptables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
                echo -e "${YELLOW}[!]${NC} Failed to add VIO port DROP rule" >&2
            iptables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
                iptables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
                echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2
            if command -v ip6tables &>/dev/null; then
                ip6tables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
                    ip6tables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
                ip6tables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
                    ip6tables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
            fi
        fi
        return 0
    fi

    [ "$ROLE" != "server" ] && return 0
    local port="${LISTEN_PORT:-8443}"

    if _is_firewalld_active; then
        firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            echo -e "${YELLOW}[!]${NC} Failed to add PREROUTING NOTRACK rule via firewalld" >&2
        firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            echo -e "${YELLOW}[!]${NC} Failed to add OUTPUT NOTRACK rule via firewalld" >&2
        firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
            firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
            echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule via firewalld" >&2
        firewall-cmd --direct --query-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            firewall-cmd --direct --add-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --query-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
            firewall-cmd --direct --add-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --query-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
            firewall-cmd --direct --add-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
    else
        local TAG="paqctl"
        modprobe iptable_raw 2>/dev/null || true
        modprobe iptable_mangle 2>/dev/null || true
        iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
            iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
            echo -e "${YELLOW}[!]${NC} Failed to add PREROUTING NOTRACK rule" >&2
        iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
            iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
            echo -e "${YELLOW}[!]${NC} Failed to add OUTPUT NOTRACK rule" >&2
        iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
            iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
            echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2
        if command -v ip6tables &>/dev/null; then
            ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
                ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
                ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
                ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
        fi
    fi
}

_remove_firewall() {
    if ! _is_firewalld_active && ! command -v iptables &>/dev/null; then
        return 0
    fi

    if [ "$BACKEND" = "gfw-knocker" ]; then
        local vio_port
        if [ "$ROLE" = "server" ]; then
            vio_port="${GFK_VIO_PORT:-45000}"
        else
            vio_port="${GFK_VIO_CLIENT_PORT:-40000}"
        fi

        if _is_firewalld_active; then
            firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
            firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
            firewall-cmd --direct --remove-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
            firewall-cmd --direct --remove-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
            firewall-cmd --direct --remove-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
            firewall-cmd --direct --remove-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        else
            local TAG="paqctl"
            iptables -t raw -D PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            iptables -t raw -D PREROUTING -p tcp --dport "$vio_port" -j NOTRACK 2>/dev/null || true
            iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -j NOTRACK 2>/dev/null || true
            iptables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
            iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
            iptables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true
            iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true
            if command -v ip6tables &>/dev/null; then
                ip6tables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
                ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
                ip6tables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true
                ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true
            fi
        fi
        return 0
    fi

    [ "$ROLE" != "server" ] && return 0
    local port="${LISTEN_PORT:-8443}"

    if _is_firewalld_active; then
        firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
        firewall-cmd --direct --remove-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
    else
        local TAG="paqctl"
        iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
        iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
        iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
        iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
        iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
        iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
        if command -v ip6tables &>/dev/null; then
            ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
            ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
            ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
            ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
            ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
        fi
    fi
}

# Remove ALL paqctl-tagged firewall rules (for complete uninstall)
_remove_all_paqctl_firewall_rules() {
    # firewalld: remove paqctl-tagged direct rules
    if _is_firewalld_active; then
        local _rules
        _rules=$(firewall-cmd --direct --get-all-rules 2>/dev/null) || true
        if [ -n "$_rules" ]; then
            echo "$_rules" | grep "paqctl" | while IFS= read -r _rule; do
                firewall-cmd --direct --remove-rule $_rule 2>/dev/null || true
                firewall-cmd --permanent --direct --remove-rule $_rule 2>/dev/null || true
            done
        fi
        return 0
    fi

    command -v iptables &>/dev/null || return 0
    local TAG="paqctl"

    # Remove all rules with "paqctl" comment from all tables
    # Loop to remove multiple rules if port was changed
    local i
    for i in {1..10}; do
        iptables -t raw -S 2>/dev/null | grep -q "paqctl" || break
        iptables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do
            # Convert -A to -D for deletion
            local del_rule="${rule/-A /-D }"
            eval "iptables -t raw $del_rule" 2>/dev/null || true
        done
    done

    for i in {1..10}; do
        iptables -t mangle -S 2>/dev/null | grep -q "paqctl" || break
        iptables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do
            local del_rule="${rule/-A /-D }"
            eval "iptables -t mangle $del_rule" 2>/dev/null || true
        done
    done

    for i in {1..10}; do
        iptables -S 2>/dev/null | grep -q "paqctl" || break
        iptables -S 2>/dev/null | grep "paqctl" | while read -r rule; do
            local del_rule="${rule/-A /-D }"
            eval "iptables $del_rule" 2>/dev/null || true
        done
    done

    # Same for IPv6
    if command -v ip6tables &>/dev/null; then
        for i in {1..10}; do
            ip6tables -t raw -S 2>/dev/null | grep -q "paqctl" || break
            ip6tables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do
                local del_rule="${rule/-A /-D }"
                eval "ip6tables -t raw $del_rule" 2>/dev/null || true
            done
        done

        for i in {1..10}; do
            ip6tables -t mangle -S 2>/dev/null | grep -q "paqctl" || break
            ip6tables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do
                local del_rule="${rule/-A /-D }"
                eval "ip6tables -t mangle $del_rule" 2>/dev/null || true
            done
        done

        for i in {1..10}; do
            ip6tables -S 2>/dev/null | grep -q "paqctl" || break
            ip6tables -S 2>/dev/null | grep "paqctl" | while read -r rule; do
                local del_rule="${rule/-A /-D }"
                eval "ip6tables $del_rule" 2>/dev/null || true
            done
        done
    fi
}

_persist_firewall() {
    if _is_firewalld_active; then
        firewall-cmd --runtime-to-permanent 2>/dev/null || true
        return 0
    fi
    if command -v netfilter-persistent &>/dev/null; then
        netfilter-persistent save 2>/dev/null || true
    elif command -v iptables-save &>/dev/null; then
        if [ -d /etc/iptables ]; then
            iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
            command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
        elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then
            mkdir -p /etc/iptables
            iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
            command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
        elif [ -d /etc/sysconfig ]; then
            iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
            command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true
        fi
    fi
}

#═══════════════════════════════════════════════════════════════════════
# Status & Info
#═══════════════════════════════════════════════════════════════════════

show_status() {
    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  PAQCTL STATUS (${BACKEND})${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""

    # Running status
    if is_running; then
        echo -e "  Status:     ${GREEN}● Running${NC}"
        # Uptime
        if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
            local started
            started=$(systemctl show paqctl.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2)
            if [ -n "$started" ]; then
                local started_ts
                started_ts=$(date -d "$started" +%s 2>/dev/null || echo 0)
                if [ "$started_ts" -gt 0 ] 2>/dev/null; then
                    local now=$(date +%s)
                    local up=$((now - started_ts))
                    local days=$((up / 86400))
                    local hours=$(( (up % 86400) / 3600 ))
                    local mins=$(( (up % 3600) / 60 ))
                    if [ "$days" -gt 0 ]; then
                        echo -e "  Uptime:     ${days}d ${hours}h ${mins}m"
                    else
                        echo -e "  Uptime:     ${hours}h ${mins}m"
                    fi
                fi
            fi
        fi
        # PID
        local pid
        if [ "$BACKEND" = "gfw-knocker" ]; then
            pid=$(pgrep -f "mainserver.py|mainclient.py" 2>/dev/null | head -1)
        else
            pid=$(pgrep -f "paqet run -c" 2>/dev/null | head -1)
        fi
        [ -n "$pid" ] && echo -e "  PID:        $pid"

        # CPU/RAM of process
        if [ -n "$pid" ]; then
            local cpu_mem
            cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1)
            if [ -n "$cpu_mem" ]; then
                local cpu=$(echo "$cpu_mem" | awk '{print $1}')
                local mem=$(echo "$cpu_mem" | awk '{print $2}')
                echo -e "  CPU:        ${cpu}%"
                echo -e "  Memory:     ${mem}%"
            fi
        fi
    else
        echo -e "  Status:     ${RED}● Stopped${NC}"
    fi

    echo ""
    echo -e "  ${DIM}── Configuration ──${NC}"
    echo -e "  Backend:    ${BOLD}${BACKEND}${NC}"
    echo -e "  Role:       ${BOLD}${ROLE}${NC}"
    echo -e "  Version:    ${PAQET_VERSION}"

    if [ "$BACKEND" = "gfw-knocker" ]; then
        echo -e "  Server IP:  ${GFK_SERVER_IP}"
        echo -e "  VIO port:   ${GFK_VIO_PORT}"
        echo -e "  QUIC port:  ${GFK_QUIC_PORT}"
        if [ "$ROLE" = "server" ]; then
            if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ] && [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                local _md=""
                IFS=',' read -ra _pairs <<< "${GFK_PORT_MAPPINGS}"
                for _p in "${_pairs[@]}"; do
                    if [ "${_p%%:*}" = "${GFK_SOCKS_VIO_PORT}" ]; then
                        _md="${_md:+${_md}, }${_p} (SOCKS5)"
                    else
                        _md="${_md:+${_md}, }${_p} (panel)"
                    fi
                done
                echo -e "  Mappings:   ${_md}"
                echo -e "  SOCKS5:     ${GREEN}127.0.0.1:${GFK_SOCKS_PORT}${NC} (server-side)"
                echo -e "  Client use: ${GREEN}127.0.0.1:${GFK_SOCKS_VIO_PORT}${NC} (set as proxy on client)"
            elif [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
                echo -e "  Mappings:   ${GFK_PORT_MAPPINGS}"
                echo -e "  SOCKS5:     ${YELLOW}not configured${NC}"
            else
                echo -e "  Mappings:   ${GFK_PORT_MAPPINGS}"
                local _srv_port _cli_port
                _srv_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f2)
                _cli_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
                echo -e "  SOCKS5:     ${GREEN}127.0.0.1:${_srv_port}${NC} (server-side)"
                echo -e "  Client use: ${GREEN}127.0.0.1:${_cli_port}${NC} (set as proxy on client)"
            fi
            echo -e "  Auth code:  ${GFK_AUTH_CODE:0:8}..."
            local _vio_port="${GFK_VIO_PORT:-45000}"
            local _input_ok=false _rst_ok=false
            if iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
               iptables -C INPUT -p tcp --dport "$_vio_port" -j DROP 2>/dev/null; then
                _input_ok=true
            fi
            if iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
               iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -j DROP 2>/dev/null; then
                _rst_ok=true
            fi
            if [ "$_input_ok" = true ] && [ "$_rst_ok" = true ]; then
                echo -e "  Firewall:   ${GREEN}VIO port blocked${NC}"
            elif [ "$_input_ok" = true ]; then
                echo -e "  Firewall:   ${YELLOW}Partial (RST DROP missing)${NC}"
            else
                echo -e "  Firewall:   ${RED}VIO port NOT blocked${NC}"
            fi
        else
            echo -e "  Mappings:   ${GFK_PORT_MAPPINGS}"
            local _fv
            if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                _fv="$GFK_SOCKS_VIO_PORT"
            else
                _fv=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
            fi
            echo -e "  Proxy:      ${GREEN}SOCKS5 127.0.0.1:${_fv}${NC} (set as browser proxy)"
        fi
    else
        echo -e "  Interface:  ${INTERFACE}"
        echo -e "  Local IP:   ${LOCAL_IP}"
        if [ "$ROLE" = "server" ]; then
            echo -e "  Port:       ${LISTEN_PORT}"
            echo -e "  Key:        ${ENCRYPTION_KEY:0:8}..."
            if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
                echo -e "  Firewall:   ${GREEN}Rules active${NC}"
            else
                echo -e "  Firewall:   ${RED}Rules missing${NC}"
            fi
        else
            echo -e "  Server:     ${REMOTE_SERVER}"
            echo -e "  SOCKS port: ${SOCKS_PORT}"
            echo -e "  Key:        ${ENCRYPTION_KEY:0:8}..."
        fi
    fi

    # Telegram
    if [ "$TELEGRAM_ENABLED" = "true" ]; then
        echo -e "  Telegram:   ${GREEN}Enabled${NC}"
    else
        echo -e "  Telegram:   ${DIM}Disabled${NC}"
    fi

    echo ""
}

#═══════════════════════════════════════════════════════════════════════
# Logs
#═══════════════════════════════════════════════════════════════════════

show_logs() {
    echo ""
    log_info "Showing paqet logs (Ctrl+C to return to menu)..."
    echo ""

    # Trap Ctrl+C to return to menu instead of exiting
    trap 'echo ""; log_info "Returning to menu..."; return 0' INT

    if command -v journalctl &>/dev/null && [ -d /run/systemd/system ]; then
        journalctl -u paqctl.service -f --no-pager -n 50
    elif [ -f /var/log/paqctl.log ]; then
        tail -f -n 50 /var/log/paqctl.log
    else
        log_warn "No logs found. Is paqet running?"
    fi

    # Restore default trap
    trap - INT
}

#═══════════════════════════════════════════════════════════════════════
# Health Check
#═══════════════════════════════════════════════════════════════════════

health_check() {
    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  HEALTH CHECK${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""

    local issues=0

    if [ "$BACKEND" = "gfw-knocker" ]; then
        # 1. Python scripts exist
        if [ -f "$GFK_DIR/mainserver.py" ] && [ -f "$GFK_DIR/mainclient.py" ]; then
            echo -e "  ${GREEN}✓${NC} GFW-knocker scripts found"
        else
            echo -e "  ${RED}✗${NC} GFW-knocker scripts missing from $GFK_DIR"
            issues=$((issues + 1))
        fi

        # 2. Python + deps (check venv)
        if [ -x "$INSTALL_DIR/venv/bin/python" ] && "$INSTALL_DIR/venv/bin/python" -c "import scapy; import aioquic" 2>/dev/null; then
            echo -e "  ${GREEN}✓${NC} Python dependencies OK (scapy, aioquic)"
        else
            echo -e "  ${RED}✗${NC} Python dependencies missing (venv not setup)"
            issues=$((issues + 1))
        fi

        # 3. Config
        if [ -f "$GFK_DIR/parameters.py" ]; then
            echo -e "  ${GREEN}✓${NC} GFK configuration found"
        else
            echo -e "  ${RED}✗${NC} GFK configuration missing"
            issues=$((issues + 1))
        fi

        # 4. Certificates
        if [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ]; then
            echo -e "  ${GREEN}✓${NC} QUIC certificates found"
        else
            echo -e "  ${RED}✗${NC} QUIC certificates missing"
            issues=$((issues + 1))
        fi

        # 5. Service running
        if is_running; then
            echo -e "  ${GREEN}✓${NC} GFW-knocker is running"
        else
            echo -e "  ${RED}✗${NC} GFW-knocker is not running"
            issues=$((issues + 1))
        fi

        # 6. Firewall (server)
        if [ "$ROLE" = "server" ]; then
            # Check both tagged and untagged rules (tagged added by _apply_firewall, untagged by install wizard)
            local _vio_port="${GFK_VIO_PORT:-45000}"
            if iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
               iptables -C INPUT -p tcp --dport "$_vio_port" -j DROP 2>/dev/null; then
                echo -e "  ${GREEN}✓${NC} VIO port ${_vio_port} INPUT blocked"
            else
                echo -e "  ${RED}✗${NC} VIO port ${_vio_port} INPUT NOT blocked"
                issues=$((issues + 1))
            fi
            # Check RST DROP rule (prevents kernel from sending RST packets)
            if iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
               iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -j DROP 2>/dev/null; then
                echo -e "  ${GREEN}✓${NC} VIO port ${_vio_port} RST DROP in place"
            else
                echo -e "  ${RED}✗${NC} VIO port ${_vio_port} RST DROP missing"
                issues=$((issues + 1))
            fi
        fi

        # 7. SOCKS5 port (client)
        if [ "$ROLE" = "client" ]; then
            local _socks_vio
            if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                _socks_vio="$GFK_SOCKS_VIO_PORT"
            else
                _socks_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
            fi
            if is_running && ss -tlnp 2>/dev/null | grep -q ":${_socks_vio} "; then
                echo -e "  ${GREEN}✓${NC} SOCKS5 port ${_socks_vio} is listening"
            elif is_running; then
                echo -e "  ${RED}✗${NC} SOCKS5 port ${_socks_vio} not listening"
                issues=$((issues + 1))
            fi
        fi
    else
        # 1. Binary exists
        if [ -x "$INSTALL_DIR/bin/paqet" ]; then
            echo -e "  ${GREEN}✓${NC} paqet binary found"
        else
            echo -e "  ${RED}✗${NC} paqet binary not found at $INSTALL_DIR/bin/paqet"
            issues=$((issues + 1))
        fi

        # 2. Config exists
        if [ -f "$INSTALL_DIR/config.yaml" ]; then
            echo -e "  ${GREEN}✓${NC} Configuration file found"
        else
            echo -e "  ${RED}✗${NC} Configuration file missing"
            issues=$((issues + 1))
        fi

        # 3. Service running
        if is_running; then
            echo -e "  ${GREEN}✓${NC} paqet is running"
        else
            echo -e "  ${RED}✗${NC} paqet is not running"
            issues=$((issues + 1))
        fi

        # 4. libpcap
        if ldconfig -p 2>/dev/null | grep -q libpcap; then
            echo -e "  ${GREEN}✓${NC} libpcap is available"
        else
            echo -e "  ${YELLOW}!${NC} libpcap not found in ldconfig (may still work)"
        fi

        # 5. iptables (server only)
        if [ "$ROLE" = "server" ]; then
            if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
                echo -e "  ${GREEN}✓${NC} iptables NOTRACK rules in place (port $LISTEN_PORT)"
            else
                echo -e "  ${RED}✗${NC} iptables NOTRACK rules missing for port $LISTEN_PORT"
                issues=$((issues + 1))
            fi

            if iptables -t mangle -C OUTPUT -p tcp --sport "$LISTEN_PORT" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null; then
                echo -e "  ${GREEN}✓${NC} iptables RST DROP rule in place"
            else
                echo -e "  ${RED}✗${NC} iptables RST DROP rule missing"
                issues=$((issues + 1))
            fi
        fi

        # 6. Port listening (server) or connectivity (client)
        if [ "$ROLE" = "server" ] && is_running; then
            if ss -tlnp 2>/dev/null | grep -q ":${LISTEN_PORT}"; then
                echo -e "  ${GREEN}✓${NC} Port $LISTEN_PORT is listening"
            else
                echo -e "  ${YELLOW}!${NC} Port $LISTEN_PORT not shown in ss (paqet uses raw sockets)"
            fi
        fi

        if [ "$ROLE" = "client" ] && is_running; then
            if ss -tlnp 2>/dev/null | grep -q ":${SOCKS_PORT}"; then
                echo -e "  ${GREEN}✓${NC} SOCKS5 port $SOCKS_PORT is listening"
            else
                echo -e "  ${RED}✗${NC} SOCKS5 port $SOCKS_PORT is not listening"
                issues=$((issues + 1))
            fi
        fi

        # 7. Paqet ping test
        if is_running && [ -x "$INSTALL_DIR/bin/paqet" ]; then
            echo -e "  ${DIM}Running paqet ping test...${NC}"
            local ping_result
            ping_result=$(timeout 10 "$INSTALL_DIR/bin/paqet" ping -c "$INSTALL_DIR/config.yaml" 2>&1 || true)
            if echo "$ping_result" | grep -qi "success\|pong\|ok\|alive\|rtt"; then
                echo -e "  ${GREEN}✓${NC} Paqet ping: OK"
            elif [ -n "$ping_result" ]; then
                echo -e "  ${YELLOW}!${NC} Paqet ping: $(echo "$ping_result" | head -1)"
            else
                echo -e "  ${YELLOW}!${NC} Paqet ping: no response (may not be supported)"
            fi
        fi
    fi

    # 8. Network connectivity
    if curl -s --max-time 5 https://api.github.com &>/dev/null; then
        echo -e "  ${GREEN}✓${NC} Internet connectivity: OK"
    else
        echo -e "  ${YELLOW}!${NC} Cannot reach GitHub API (may be firewall/network)"
    fi

    # 9. Systemd service
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        if systemctl is-enabled paqctl.service &>/dev/null; then
            echo -e "  ${GREEN}✓${NC} Auto-start on boot: enabled"
        else
            echo -e "  ${YELLOW}!${NC} Auto-start on boot: disabled"
        fi
    fi

    echo ""
    if [ "$issues" -eq 0 ]; then
        echo -e "  ${GREEN}${BOLD}All checks passed!${NC}"
    else
        echo -e "  ${RED}${BOLD}$issues issue(s) found${NC}"
    fi
    echo ""
}

#═══════════════════════════════════════════════════════════════════════
# Update
#═══════════════════════════════════════════════════════════════════════

update_gfk() {
    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  UPDATE GFW-KNOCKER${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""

    log_info "Downloading latest GFW-knocker scripts..."
    local tmp_dir
    tmp_dir=$(mktemp -d)
    local server_files="mainserver.py quic_server.py vio_server.py"
    local client_files="mainclient.py quic_client.py vio_client.py"
    local f changed=false
    # Download server scripts
    for f in $server_files; do
        if ! curl -sL "$GFK_RAW_URL/server/$f" -o "$tmp_dir/$f"; then
            log_error "Failed to download $f"
            rm -rf "$tmp_dir"
            return 1
        fi
        if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then
            changed=true
        fi
    done
    # Download client scripts
    for f in $client_files; do
        if ! curl -sL "$GFK_RAW_URL/client/$f" -o "$tmp_dir/$f"; then
            log_error "Failed to download $f"
            rm -rf "$tmp_dir"
            return 1
        fi
        if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then
            changed=true
        fi
    done

    if [ "$changed" = true ]; then
        local was_running=false
        is_running && was_running=true
        [ "$was_running" = true ] && stop_paqet

        # Backup old scripts
        mkdir -p "$BACKUP_DIR"
        local all_files="$server_files $client_files"
        for f in $all_files; do
            [ -f "$GFK_DIR/$f" ] && cp "$GFK_DIR/$f" "$BACKUP_DIR/${f}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
        done

        for f in $all_files; do
            cp "$tmp_dir/$f" "$GFK_DIR/$f"
        done
        chmod 600 "$GFK_DIR"/*.py
        # Patch mainserver.py to use venv python for subprocesses
        [ -f "$GFK_DIR/mainserver.py" ] && sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py"
        log_success "GFW-knocker scripts updated"

        # Also upgrade Python deps in venv
        "$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true

        [ "$was_running" = true ] && start_paqet
    else
        log_success "GFW-knocker scripts are already up to date"
        "$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true
    fi
    rm -rf "$tmp_dir"

    # Regenerate client wrapper (removes legacy microsocks startup)
    if [ "$ROLE" = "client" ]; then
        create_gfk_client_wrapper
        pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true
    fi

    # Also check for management script updates
    update_management_script
    echo ""
}

update_paqet() {
    if [ "$BACKEND" = "gfw-knocker" ]; then
        update_gfk
        return
    fi

    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  UPDATE PAQET${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""

    log_info "Querying GitHub for latest release..."

    # Get latest version from GitHub with retry
    local response
    response=$(_curl_with_retry "$PAQET_API_URL" 3)
    if [ -z "$response" ]; then
        log_error "Failed to query GitHub API after retries. Check your internet connection."
        return 1
    fi

    local latest_tag
    latest_tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
    if [ -z "$latest_tag" ] || ! _validate_version_tag "$latest_tag"; then
        log_error "Could not determine valid version from GitHub"
        return 1
    fi

    # Extract release date
    local release_date
    release_date=$(echo "$response" | grep -o '"published_at"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"' | cut -dT -f1)

    # Extract release notes (body field)
    local release_notes=""
    if command -v python3 &>/dev/null; then
        release_notes=$(python3 -c "
import json,sys
try:
    d=json.loads(sys.stdin.read())
    body=d.get('body','')
    if body:
        # Truncate to first 500 chars, strip markdown
        body=body[:500].replace('**','').replace('##','').replace('# ','')
        print(body)
except: pass
" <<< "$response" 2>/dev/null)
    fi

    local current="${PAQET_VERSION:-unknown}"
    local bin_ver
    bin_ver=$("$INSTALL_DIR/bin/paqet" version 2>/dev/null || echo "unknown")

    echo ""
    echo -e "  ${DIM}── Version Info ──${NC}"
    echo -e "  Installed version:  ${BOLD}${current}${NC}"
    echo -e "  Binary reports:     ${BOLD}${bin_ver}${NC}"
    echo -e "  Latest release:     ${BOLD}${latest_tag}${NC}"
    [ -n "$release_date" ] && echo -e "  Release date:       ${release_date}"

    if [ "$current" = "$latest_tag" ]; then
        echo ""
        log_success "You are already on the latest version!"
        echo ""
        echo -e "  ${DIM}Options:${NC}"
        echo "  1. Force reinstall current version"
        echo "  2. Rollback to previous version"
        echo "  3. Update management script only"
        echo "  b. Back"
        echo ""
        read -p "  Choice: " up_choice < /dev/tty || true
        case "$up_choice" in
            1)
                read -p "  Force reinstall ${current}? [y/N]: " _fc < /dev/tty || true
                [[ "$_fc" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; }
                ;;
            2) rollback_paqet; return ;;
            3) update_management_script; return ;;
            [bB]) return 0 ;;
            *) return 0 ;;
        esac
    fi

    # Show release notes if available
    if [ -n "$release_notes" ]; then
        echo ""
        echo -e "  ${DIM}── Release Notes ──${NC}"
        echo "$release_notes" | while IFS= read -r line; do
            echo -e "  ${DIM}${line}${NC}"
        done
        echo ""
    fi

    echo ""
    echo -e "${YELLOW}╔════════════════════════════════════════════════════════════════╗${NC}"
    echo -e "${YELLOW}║  ${BOLD}⚠ WARNING: Updating may cause compatibility issues!${NC}${YELLOW}            ║${NC}"
    echo -e "${YELLOW}╠════════════════════════════════════════════════════════════════╣${NC}"
    echo -e "${YELLOW}║${NC}  paqctl was tested with: ${BOLD}${PAQET_VERSION_PINNED}${NC}"
    echo -e "${YELLOW}║${NC}  Newer versions may have breaking changes or bugs."
    echo -e "${YELLOW}║${NC}  You can rollback with: ${BOLD}sudo paqctl rollback${NC}"
    echo -e "${YELLOW}╚════════════════════════════════════════════════════════════════╝${NC}"
    echo ""
    read -p "  Update to ${latest_tag}? [y/N]: " confirm < /dev/tty || true
    if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
        log_info "Update cancelled"
        return 0
    fi

    # Download new binary
    _download_and_install_binary "$latest_tag" || return 1

    # Check for management script update
    update_management_script
}

_download_and_install_binary() {
    local target_tag="$1"
    local arch
    arch=$(uname -m)
    case "$arch" in
        x86_64|amd64) arch="amd64" ;;
        aarch64|arm64) arch="arm64" ;;
        *) log_error "Unsupported architecture: $arch"; return 1 ;;
    esac

    local filename="paqet-linux-${arch}-${target_tag}.tar.gz"
    local url="https://github.com/${PAQET_REPO}/releases/download/${target_tag}/${filename}"
    local tmp_file
    tmp_file=$(mktemp "/tmp/paqet-update-XXXXXXXX.tar.gz")

    log_info "Downloading ${filename}..."
    if ! curl -sL --max-time 120 --fail -o "$tmp_file" "$url"; then
        log_error "Download failed: $url"
        rm -f "$tmp_file"
        return 1
    fi

    # Validate
    local fsize
    fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
    if [ "$fsize" -lt 1000 ]; then
        log_error "Downloaded file too small ($fsize bytes). Aborting."
        rm -f "$tmp_file"
        return 1
    fi

    # Extract
    local tmp_extract
    tmp_extract=$(mktemp -d "/tmp/paqet-update-extract-XXXXXXXX")
    if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
        log_error "Failed to extract archive"
        rm -f "$tmp_file"
        rm -rf "$tmp_extract"
        return 1
    fi

    local binary_name="paqet_linux_${arch}"
    local found_binary
    found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
    [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
    [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)

    if [ -z "$found_binary" ]; then
        log_error "Could not find paqet binary in archive"
        rm -f "$tmp_file"
        rm -rf "$tmp_extract"
        return 1
    fi

    # Stop service, replace, start
    local was_running=false
    if is_running; then
        was_running=true
        stop_paqet
    fi

    # Backup old binary with version tag for rollback
    if ! mkdir -p "$BACKUP_DIR"; then
        log_warn "Failed to create backup directory"
    fi
    local old_ver="${PAQET_VERSION:-unknown}"
    cp "$INSTALL_DIR/bin/paqet" "$BACKUP_DIR/paqet.${old_ver}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true

    if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
        log_error "Failed to copy new binary"
        rm -f "$tmp_file"
        rm -rf "$tmp_extract"
        # Restore from backup
        local latest_backup
        latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1)
        [ -n "$latest_backup" ] && cp "$latest_backup" "$INSTALL_DIR/bin/paqet" && chmod +x "$INSTALL_DIR/bin/paqet"
        [ "$was_running" = true ] && start_paqet
        return 1
    fi
    chmod +x "$INSTALL_DIR/bin/paqet"

    rm -f "$tmp_file"
    rm -rf "$tmp_extract"

    # Verify the new binary works
    if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
        log_warn "New binary failed verification. Restoring backup..."
        local latest_backup
        latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1)
        if [ -n "$latest_backup" ]; then
            cp "$latest_backup" "$INSTALL_DIR/bin/paqet"
            chmod +x "$INSTALL_DIR/bin/paqet"
            log_error "Update failed — previous version restored"
            return 1
        fi
        log_error "Update failed and no backup available"
        return 1
    fi

    # Update version in settings
    PAQET_VERSION="$target_tag"
    _safe_update_setting "PAQET_VERSION" "$target_tag" "$INSTALL_DIR/settings.conf"

    log_success "paqet updated to ${target_tag}"

    if [ "$was_running" = true ]; then
        start_paqet
    fi
}

rollback_paqet() {
    echo ""
    if [ ! -d "$BACKUP_DIR" ]; then
        log_warn "No backups found"
        return 1
    fi

    local backups=()
    local i=1
    echo -e "  ${BOLD}Available binary backups:${NC}"
    echo ""
    for f in "$BACKUP_DIR"/paqet.*; do
        [ -f "$f" ] || continue
        backups+=("$f")
        local bname=$(basename "$f")
        local bsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || wc -c < "$f" 2>/dev/null || echo "?")
        echo "  $i. $bname  (${bsize} bytes)"
        i=$((i + 1))
    done

    if [ ${#backups[@]} -eq 0 ]; then
        log_warn "No binary backups found in $BACKUP_DIR"
        return 1
    fi

    echo ""
    echo "  0. Cancel"
    echo ""
    read -p "  Select backup to restore [0-${#backups[@]}]: " choice < /dev/tty || true
    if [ "$choice" = "0" ]; then
        log_info "Cancelled"
        return 0
    fi
    if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then
        log_error "Invalid choice"
        return 1
    fi

    local selected="${backups[$((choice-1))]}"
    log_info "Rolling back to: $(basename "$selected")"

    local was_running=false
    is_running && was_running=true
    [ "$was_running" = true ] && stop_paqet

    if ! cp "$selected" "$INSTALL_DIR/bin/paqet"; then
        log_error "Failed to restore backup"
        [ "$was_running" = true ] && start_paqet
        return 1
    fi
    chmod +x "$INSTALL_DIR/bin/paqet"

    # Verify restored binary
    if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
        log_warn "Restored binary failed verification (may need libpcap)"
    fi

    # Try to extract version from the filename (format: paqet.vX.Y.Z.TIMESTAMP)
    local restored_ver=""
    local _bname
    _bname=$(basename "$selected")
    # Extract version: remove 'paqet.' prefix and '.YYYYMMDDHHMMSS' timestamp suffix
    restored_ver=$(echo "$_bname" | sed 's/^paqet\.//' | sed 's/\.[0-9]\{14\}$//')
    # Validate extracted version looks reasonable
    if [ -n "$restored_ver" ] && [ "$restored_ver" != "backup" ] && [ "$restored_ver" != "$_bname" ]; then
        if _validate_version_tag "$restored_ver"; then
            PAQET_VERSION="$restored_ver"
            _safe_update_setting "PAQET_VERSION" "$restored_ver" "$INSTALL_DIR/settings.conf"
            log_info "Restored version: $restored_ver"
        else
            log_warn "Could not determine version from backup filename, keeping current version setting"
        fi
    else
        log_warn "Could not extract version from backup filename"
    fi

    log_success "Rolled back successfully"

    [ "$was_running" = true ] && start_paqet
}

update_management_script() {
    local update_url="https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh"
    local tmp_script
    tmp_script=$(mktemp "/tmp/paqctl-update-XXXXXXXX.sh")

    log_info "Checking for management script updates..."
    if ! curl -sL --max-time 30 --max-filesize 2097152 -o "$tmp_script" "$update_url" 2>/dev/null; then
        log_warn "Could not check for script updates"
        rm -f "$tmp_script"
        return 0
    fi

    # Validate: must contain our markers, be a bash script, and pass syntax check
    if ! head -n 1 "$tmp_script" 2>/dev/null | grep -q "^#!.*bash"; then
        log_warn "Downloaded file is not a bash script, skipping"
        rm -f "$tmp_script"
        return 0
    fi
    if grep -q "PAQET_REPO=" "$tmp_script" && \
       grep -q "create_management_script" "$tmp_script" && \
       grep -q "PAQCTL_VERSION=" "$tmp_script" && \
       bash -n "$tmp_script" 2>/dev/null; then
        local _update_output
        if _update_output=$(bash "$tmp_script" --update-components 2>&1); then
            log_success "Management script updated"
        else
            log_warn "Management script update execution failed: ${_update_output:-unknown error}"
        fi
    else
        log_warn "Downloaded script failed validation, skipping"
    fi
    rm -f "$tmp_script"
}

#═══════════════════════════════════════════════════════════════════════
# Secret Key Generation
#═══════════════════════════════════════════════════════════════════════

generate_secret() {
    echo ""
    local key
    key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
    if [ -z "$key" ]; then
        key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32)
    fi
    echo -e "  ${GREEN}${BOLD}New encryption key: ${key}${NC}"
    echo ""
    echo -e "  ${DIM}Share this key securely with client users.${NC}"
    echo ""
}

#═══════════════════════════════════════════════════════════════════════
# Firewall Display
#═══════════════════════════════════════════════════════════════════════

show_firewall() {
    if [ "$ROLE" != "server" ] && [ "$BACKEND" != "gfw-knocker" ]; then
        echo ""
        log_info "Firewall rules only apply in server mode or GFK client mode"
        echo ""
        return
    fi

    local redraw=true
    while true; do
        if [ "$redraw" = true ]; then
            echo ""
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo -e "${BOLD}  FIREWALL RULES${NC}"
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo ""

            local _fw_backend="iptables"
            _is_firewalld_active && _fw_backend="firewalld"
            echo -e "  ${DIM}Backend: ${_fw_backend}${NC}"
            echo ""

            if [ "$BACKEND" = "gfw-knocker" ]; then
                local vio_port
                if [ "$ROLE" = "server" ]; then
                    vio_port="${GFK_VIO_PORT:-45000}"
                else
                    vio_port="${GFK_VIO_CLIENT_PORT:-40000}"
                fi
                echo -e "  ${BOLD}Required rules for VIO port ${vio_port}:${NC}"
                echo ""
                if [ "$_fw_backend" = "firewalld" ]; then
                    firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} PREROUTING NOTRACK (dport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} PREROUTING NOTRACK (dport $vio_port)  ${DIM}MISSING${NC}"
                    firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} OUTPUT NOTRACK (sport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} OUTPUT NOTRACK (sport $vio_port)  ${DIM}MISSING${NC}"
                    firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} INPUT DROP (dport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} INPUT DROP (dport $vio_port)  ${DIM}MISSING${NC}"
                    firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} RST DROP (sport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} RST DROP (sport $vio_port)  ${DIM}MISSING${NC}"
                else
                    iptables -t raw -C PREROUTING -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} PREROUTING NOTRACK (dport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} PREROUTING NOTRACK (dport $vio_port)  ${DIM}MISSING${NC}"
                    iptables -t raw -C OUTPUT -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} OUTPUT NOTRACK (sport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} OUTPUT NOTRACK (sport $vio_port)  ${DIM}MISSING${NC}"
                    iptables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} INPUT DROP (dport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} INPUT DROP (dport $vio_port)  ${DIM}MISSING${NC}"
                    iptables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} RST DROP (sport $vio_port)" \
                        || echo -e "  ${RED}✗${NC} RST DROP (sport $vio_port)  ${DIM}MISSING${NC}"
                fi
            else
                local port="${LISTEN_PORT:-8443}"
                echo -e "  ${BOLD}Required rules for port ${port}:${NC}"
                echo ""
                if [ "$_fw_backend" = "firewalld" ]; then
                    firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)" \
                        || echo -e "  ${RED}✗${NC} PREROUTING NOTRACK (dport $port)  ${DIM}MISSING${NC}"
                    firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)" \
                        || echo -e "  ${RED}✗${NC} OUTPUT NOTRACK (sport $port)  ${DIM}MISSING${NC}"
                    firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} RST DROP (sport $port)" \
                        || echo -e "  ${RED}✗${NC} RST DROP (sport $port)  ${DIM}MISSING${NC}"
                else
                    iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)" \
                        || echo -e "  ${RED}✗${NC} PREROUTING NOTRACK (dport $port)  ${DIM}MISSING${NC}"
                    iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)" \
                        || echo -e "  ${RED}✗${NC} OUTPUT NOTRACK (sport $port)  ${DIM}MISSING${NC}"
                    iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null \
                        && echo -e "  ${GREEN}✓${NC} RST DROP (sport $port)" \
                        || echo -e "  ${RED}✗${NC} RST DROP (sport $port)  ${DIM}MISSING${NC}"
                fi
            fi

            echo ""
            echo -e "  ${BOLD}Actions:${NC}"
            echo "  1. Apply missing rules"
            echo "  2. Remove all rules"
            echo "  b. Back"
            echo ""
            redraw=false
        fi

        read -p "  Choice: " fw_choice < /dev/tty || break
        case "$fw_choice" in
            1)
                _apply_firewall
                _persist_firewall
                log_success "Firewall rules applied and persisted"
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            2)
                _remove_firewall
                _persist_firewall
                log_success "Firewall rules removed"
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            b|B) return ;;
            "") ;;
            *) echo -e "  ${RED}Invalid choice${NC}" ;;
        esac
    done
}

#═══════════════════════════════════════════════════════════════════════
# Configuration
#═══════════════════════════════════════════════════════════════════════

_change_config_gfk() {
    local was_running="$1"
    echo ""
    echo -e "${BOLD}Select role:${NC}"
    echo "  1. Server"
    echo "  2. Client"
    echo ""
    local role_choice
    read -p "  Enter choice [1/2]: " role_choice < /dev/tty || true
    case "$role_choice" in
        1) ROLE="server" ;;
        2) ROLE="client" ;;
        *) log_warn "Invalid. Keeping current role: $ROLE" ;;
    esac

    if [ "$ROLE" = "server" ]; then
        echo -e "${BOLD}Server public IP${NC} [${GFK_SERVER_IP}]:"
        read -p "  IP: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_ip "$input"; then
            log_error "Invalid IP address"; return 1
        fi
        [ -n "$input" ] && GFK_SERVER_IP="$input"

        echo -e "${BOLD}VIO TCP port${NC} [${GFK_VIO_PORT:-45000}]:"
        read -p "  Port: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_port "$input"; then
            log_error "Invalid port number"; return 1
        fi
        [ -n "$input" ] && GFK_VIO_PORT="$input"

        echo -e "${BOLD}QUIC port${NC} [${GFK_QUIC_PORT:-25000}]:"
        read -p "  Port: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_port "$input"; then
            log_error "Invalid port number"; return 1
        fi
        [ -n "$input" ] && GFK_QUIC_PORT="$input"

        echo -e "${BOLD}Auth code${NC} [keep current]:"
        read -p "  Code: " input < /dev/tty || true
        [ -n "$input" ] && GFK_AUTH_CODE="$input"

        echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:"
        read -p "  Mappings: " input < /dev/tty || true
        [ -n "$input" ] && GFK_PORT_MAPPINGS="$input"

        echo -e "${BOLD}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:"
        echo -e "  ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}"
        echo -e "  ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}"
        read -p "  Flags: " input < /dev/tty || true
        if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then
            log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1
        fi
        [ -n "$input" ] && GFK_TCP_FLAGS="$input"
    else
        echo -e "${BOLD}Server IP${NC} [${GFK_SERVER_IP}]:"
        read -p "  IP: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_ip "$input"; then
            log_error "Invalid IP address"; return 1
        fi
        [ -n "$input" ] && GFK_SERVER_IP="$input"

        echo -e "${BOLD}Server's VIO TCP port${NC} [${GFK_VIO_PORT:-45000}] (must match server):"
        read -p "  Port: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_port "$input"; then
            log_error "Invalid port number"; return 1
        fi
        [ -n "$input" ] && GFK_VIO_PORT="$input"

        echo -e "${BOLD}Local VIO client port${NC} [${GFK_VIO_CLIENT_PORT:-40000}]:"
        read -p "  Port: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_port "$input"; then
            log_error "Invalid port number"; return 1
        fi
        [ -n "$input" ] && GFK_VIO_CLIENT_PORT="$input"

        echo -e "${BOLD}Server's QUIC port${NC} [${GFK_QUIC_PORT:-25000}] (must match server):"
        read -p "  Port: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_port "$input"; then
            log_error "Invalid port number"; return 1
        fi
        [ -n "$input" ] && GFK_QUIC_PORT="$input"

        echo -e "${BOLD}Local QUIC client port${NC} [${GFK_QUIC_CLIENT_PORT:-20000}]:"
        read -p "  Port: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_port "$input"; then
            log_error "Invalid port number"; return 1
        fi
        [ -n "$input" ] && GFK_QUIC_CLIENT_PORT="$input"

        echo -e "${BOLD}Auth code${NC}:"
        read -p "  Code: " input < /dev/tty || true
        [ -n "$input" ] && GFK_AUTH_CODE="$input"

        echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:"
        read -p "  Mappings: " input < /dev/tty || true
        [ -n "$input" ] && GFK_PORT_MAPPINGS="$input"

        echo -e "${BOLD}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:"
        echo -e "  ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}"
        echo -e "  ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}"
        read -p "  Flags: " input < /dev/tty || true
        if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then
            log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1
        fi
        [ -n "$input" ] && GFK_TCP_FLAGS="$input"
    fi

    # Regenerate parameters.py
    generate_gfk_config || { [ "$was_running" = true ] && start_paqet; return 1; }

    # Regenerate wrapper if client
    if [ "$ROLE" = "client" ]; then
        create_gfk_client_wrapper
    fi

    # Save settings
    local IFACE="" GW_MAC=""
    save_settings

    # Re-apply firewall
    _apply_firewall

    # Restart
    [ "$was_running" = true ] && start_paqet
    log_success "GFW-knocker configuration updated"
}

change_config() {
    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  CHANGE CONFIGURATION${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""
    local _warn_text="config"
    [ "$BACKEND" = "gfw-knocker" ] && _warn_text="parameters.py"
    echo -e "  ${YELLOW}Warning: This will regenerate ${_warn_text} and restart ${BACKEND}.${NC}"
    echo ""
    read -p "  Continue? [y/N]: " confirm < /dev/tty || true
    if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
        return 0
    fi

    local was_running=false
    is_running && was_running=true
    [ "$was_running" = true ] && stop_paqet

    if [ "$BACKEND" = "gfw-knocker" ]; then
        _remove_firewall
        _change_config_gfk "$was_running"
        return
    fi

    # Remove old firewall rules (save old port before user changes it)
    local _saved_port="$LISTEN_PORT"
    if [ "$ROLE" = "server" ] && [ -n "$_saved_port" ]; then
        _remove_firewall
    fi

    # Re-run wizard (inline version)
    echo ""
    echo -e "${BOLD}Select role:${NC}"
    echo "  1. Server"
    echo "  2. Client"
    echo ""
    local role_choice
    read -p "  Enter choice [1/2]: " role_choice < /dev/tty || true
    case "$role_choice" in
        1) ROLE="server" ;;
        2) ROLE="client" ;;
        *)
            log_warn "Invalid choice. Defaulting to server."
            ROLE="server"
            ;;
    esac

    # Detect network
    local _iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
    # Note: grep returns exit 1 if no matches, so we add || true for pipefail
    local _ip=$(ip -4 addr show "$_iface" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1)
    local _gw=$(ip route show default 2>/dev/null | awk '{print $3; exit}')
    local _gw_mac=""
    [ -n "$_gw" ] && _gw_mac=$(ip neigh show "$_gw" 2>/dev/null | awk '/lladdr/{print $5; exit}')

    echo ""
    echo -e "${BOLD}Interface${NC} [${_iface:-$INTERFACE}]:"
    read -p "  Interface: " input < /dev/tty || true
    INTERFACE="${input:-${_iface:-$INTERFACE}}"

    echo -e "${BOLD}Local IP${NC} [${_ip:-$LOCAL_IP}]:"
    read -p "  IP: " input < /dev/tty || true
    LOCAL_IP="${input:-${_ip:-$LOCAL_IP}}"

    echo -e "${BOLD}Gateway MAC${NC} [${_gw_mac:-$GATEWAY_MAC}]:"
    read -p "  MAC: " input < /dev/tty || true
    GATEWAY_MAC="${input:-${_gw_mac:-$GATEWAY_MAC}}"
    if [ -n "$GATEWAY_MAC" ] && ! _validate_mac "$GATEWAY_MAC"; then
        log_warn "Invalid MAC address format (expected: aa:bb:cc:dd:ee:ff)"
        read -p "  Enter valid MAC address: " input < /dev/tty || true
        if [ -n "$input" ] && ! _validate_mac "$input"; then
            log_warn "Invalid MAC format, keeping current value"
            input=""
        fi
        [ -n "$input" ] && GATEWAY_MAC="$input"
    fi

    if [ "$ROLE" = "server" ]; then
        echo -e "${BOLD}Port${NC} [${LISTEN_PORT:-8443}]:"
        read -p "  Port: " input < /dev/tty || true
        LISTEN_PORT="${input:-${LISTEN_PORT:-8443}}"
        if ! _validate_port "$LISTEN_PORT"; then
            log_warn "Invalid port. Using default 8443."
            LISTEN_PORT=8443
        fi

        echo -e "${BOLD}Encryption key${NC} [keep current]:"
        read -p "  Key (enter to keep): " input < /dev/tty || true
        [ -n "$input" ] && ENCRYPTION_KEY="$input"
        REMOTE_SERVER=""
        SOCKS_PORT=""
    else
        echo -e "${BOLD}Remote server${NC} (IP:PORT):"
        read -p "  Server: " input < /dev/tty || true
        REMOTE_SERVER="${input:-$REMOTE_SERVER}"

        echo -e "${BOLD}Encryption key${NC}:"
        read -p "  Key: " input < /dev/tty || true
        [ -n "$input" ] && ENCRYPTION_KEY="$input"

        echo -e "${BOLD}SOCKS5 port${NC} [${SOCKS_PORT:-1080}]:"
        read -p "  Port: " input < /dev/tty || true
        SOCKS_PORT="${input:-${SOCKS_PORT:-1080}}"
        LISTEN_PORT=""
    fi

    # TCP flags (for both server and client)
    echo -e "${BOLD}TCP local flag${NC} [${PAQET_TCP_LOCAL_FLAG:-PA}]:"
    echo -e "  ${DIM}Controls TCP flags on outgoing packets (default: PA = PSH+ACK)${NC}"
    echo -e "  ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG) E(ECE) C(CWR)${NC}"
    echo -e "  ${DIM}Multiple values: PA,A (tries PA first, then A)${NC}"
    read -p "  Flag: " input < /dev/tty || true
    if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then
        log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value."
        input=""
    fi
    [ -n "$input" ] && PAQET_TCP_LOCAL_FLAG="$input"

    echo -e "${BOLD}TCP remote flag${NC} [${PAQET_TCP_REMOTE_FLAG:-PA}]:"
    echo -e "  ${DIM}Controls expected TCP flags on incoming packets (default: PA)${NC}"
    echo -e "  ${DIM}Should match the server/client counterpart's local flag${NC}"
    read -p "  Flag: " input < /dev/tty || true
    if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then
        log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value."
        input=""
    fi
    [ -n "$input" ] && PAQET_TCP_REMOTE_FLAG="$input"

    # Save
    local IFACE="$INTERFACE"
    local GW_MAC="$GATEWAY_MAC"
    # Regenerate YAML
    local tmp_conf
    tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX")
    # Validate required fields
    if [ -z "$INTERFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GATEWAY_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then
        log_error "Missing required configuration fields"
        rm -f "$tmp_conf"
        [ "$was_running" = true ] && start_paqet
        return 1
    fi

    # Escape YAML special characters to prevent injection
    _escape_yaml() {
        local s="$1"
        if [[ "$s" =~ [:\#\[\]{}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
            s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s"
        else
            printf '%s' "$s"
        fi
    }
    # Set permissions before writing
    chmod 600 "$tmp_conf" 2>/dev/null
    (
    umask 077
    local _y_iface _y_ip _y_mac _y_key _y_server _tcp_local_flags _tcp_remote_flags
    _y_iface=$(_escape_yaml "$INTERFACE")
    _y_ip=$(_escape_yaml "$LOCAL_IP")
    _y_mac=$(_escape_yaml "$GATEWAY_MAC")
    _y_key=$(_escape_yaml "$ENCRYPTION_KEY")
    _tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
    _tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
    if [ "$ROLE" = "server" ]; then
        cat > "$tmp_conf" << EOF
role: "server"

log:
  level: "info"

listen:
  addr: ":${LISTEN_PORT}"

network:
  interface: "${_y_iface}"
  ipv4:
    addr: "${_y_ip}:${LISTEN_PORT}"
    router_mac: "${_y_mac}"
  tcp:
    local_flag: ${_tcp_local_flags}
    remote_flag: ${_tcp_remote_flags}

transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_y_key}"
EOF
    else
        _y_server=$(_escape_yaml "$REMOTE_SERVER")
        cat > "$tmp_conf" << EOF
role: "client"

log:
  level: "info"

socks5:
  - listen: "127.0.0.1:${SOCKS_PORT}"

network:
  interface: "${_y_iface}"
  ipv4:
    addr: "${_y_ip}:0"
    router_mac: "${_y_mac}"
  tcp:
    local_flag: ${_tcp_local_flags}
    remote_flag: ${_tcp_remote_flags}

server:
  addr: "${_y_server}"

transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_y_key}"
EOF
    fi
    )
    if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
        log_error "Failed to save configuration"
        rm -f "$tmp_conf"
        [ "$was_running" = true ] && start_paqet
        return 1
    fi
    chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null

    # Save settings
    local _tmp
    _tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX")
    # Read current telegram settings
    local _tg_token="${TELEGRAM_BOT_TOKEN:-}"
    local _tg_chat="${TELEGRAM_CHAT_ID:-}"
    local _tg_interval="${TELEGRAM_INTERVAL:-6}"
    local _tg_enabled="${TELEGRAM_ENABLED:-false}"
    local _tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}"
    local _tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}"
    local _tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}"
    local _tg_label="${TELEGRAM_SERVER_LABEL:-}"
    local _tg_start_hour="${TELEGRAM_START_HOUR:-0}"
    (
    umask 077
    cat > "$_tmp" << EOF
BACKEND="${BACKEND:-paqet}"
ROLE="${ROLE}"
PAQET_VERSION="${PAQET_VERSION}"
PAQCTL_VERSION="${VERSION}"
LISTEN_PORT="${LISTEN_PORT:-}"
SOCKS_PORT="${SOCKS_PORT:-}"
INTERFACE="${INTERFACE}"
LOCAL_IP="${LOCAL_IP}"
GATEWAY_MAC="${GATEWAY_MAC}"
ENCRYPTION_KEY="${ENCRYPTION_KEY}"
REMOTE_SERVER="${REMOTE_SERVER:-}"
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
GFK_AUTH_CODE="${GFK_AUTH_CODE:-}"
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}"
GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}"
XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}"
MICROSOCKS_PORT="${MICROSOCKS_PORT:-}"
GFK_SERVER_IP="${GFK_SERVER_IP:-}"
GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}"
PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}"
PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}"
TELEGRAM_BOT_TOKEN="${_tg_token}"
TELEGRAM_CHAT_ID="${_tg_chat}"
TELEGRAM_INTERVAL=${_tg_interval}
TELEGRAM_ENABLED=${_tg_enabled}
TELEGRAM_ALERTS_ENABLED=${_tg_alerts}
TELEGRAM_DAILY_SUMMARY=${_tg_daily}
TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly}
TELEGRAM_SERVER_LABEL="${_tg_label}"
TELEGRAM_START_HOUR=${_tg_start_hour}
EOF
    )
    if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then
        log_error "Failed to save settings"
        rm -f "$_tmp"
    fi
    chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null

    log_success "Configuration updated"

    if [ "$was_running" = true ]; then
        start_paqet
    fi
}

#═══════════════════════════════════════════════════════════════════════
# Backup & Restore
#═══════════════════════════════════════════════════════════════════════

backup_config() {
    (umask 077; mkdir -p "$BACKUP_DIR")
    chmod 700 "$BACKUP_DIR" 2>/dev/null
    local ts=$(date +%Y%m%d%H%M%S)
    local backup_file="$BACKUP_DIR/paqctl-backup-${ts}.tar.gz"

    if ! (umask 077; tar -czf "$backup_file" \
        -C "$INSTALL_DIR" \
        config.yaml settings.conf 2>/dev/null); then
        log_error "Failed to create backup archive"
        rm -f "$backup_file"
        return 1
    fi
    echo ""
    log_success "Backup saved to: $backup_file"
    echo ""
}

restore_config() {
    echo ""
    if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR"/*.tar.gz 2>/dev/null)" ]; then
        log_warn "No backups found in $BACKUP_DIR"
        return 1
    fi

    echo -e "${BOLD}Available backups:${NC}"
    echo ""
    local i=1
    local backups=()
    for f in "$BACKUP_DIR"/*.tar.gz; do
        backups+=("$f")
        echo "  $i. $(basename "$f")"
        i=$((i + 1))
    done
    echo ""
    echo "  0. Cancel"
    echo ""
    read -p "  Select backup [0-${#backups[@]}]: " choice < /dev/tty || true
    if [ "$choice" = "0" ]; then
        log_info "Cancelled"
        return 0
    fi
    if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then
        log_error "Invalid choice"
        return 1
    fi

    local selected="${backups[$((choice-1))]}"
    log_info "Restoring from: $(basename "$selected")"

    local was_running=false
    is_running && was_running=true
    [ "$was_running" = true ] && stop_paqet

    if ! (umask 077; tar -xzf "$selected" -C "$INSTALL_DIR" 2>/dev/null); then
        log_error "Failed to extract backup archive"
        [ "$was_running" = true ] && start_paqet
        return 1
    fi
    chmod 600 "$INSTALL_DIR/config.yaml" "$INSTALL_DIR/settings.conf" 2>/dev/null
    chown root:root "$INSTALL_DIR/config.yaml" "$INSTALL_DIR/settings.conf" 2>/dev/null

    # Reload settings
    _load_settings

    log_success "Configuration restored"

    [ "$was_running" = true ] && start_paqet
}

#═══════════════════════════════════════════════════════════════════════
# Telegram Integration
#═══════════════════════════════════════════════════════════════════════

# Secure Telegram API curl - writes token to temp file to avoid /proc exposure
_telegram_api_curl() {
    local endpoint="$1"
    shift
    local _tg_tmp
    _tg_tmp=$(mktemp "${INSTALL_DIR}/.tg_curl.XXXXXXXX") || return 1
    chmod 600 "$_tg_tmp" 2>/dev/null
    printf 'url = "https://api.telegram.org/bot%s/%s"\n' "$TELEGRAM_BOT_TOKEN" "$endpoint" > "$_tg_tmp"
    local _result
    _result=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_tg_tmp" "$@" 2>/dev/null)
    local _exit=$?
    rm -f "$_tg_tmp"
    [ $_exit -eq 0 ] && echo "$_result"
    return $_exit
}

escape_telegram_markdown() {
    local text="$1"
    text="${text//\\/\\\\}"
    text="${text//\*/\\*}"
    text="${text//_/\\_}"
    text="${text//\`/\\\`}"
    text="${text//\[/\\[}"
    text="${text//\]/\\]}"
    echo "$text"
}

telegram_send_message() {
    local message="$1"
    { [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && return 1
    local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
    label=$(escape_telegram_markdown "$label")
    local _ip=$(curl -s --max-time 3 https://api.ipify.org 2>/dev/null || echo "")
    if [ -n "$_ip" ]; then
        message="[${label} | ${_ip}] ${message}"
    else
        message="[${label}] ${message}"
    fi
    local response
    response=$(_telegram_api_curl "sendMessage" \
        -X POST \
        --data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
        --data-urlencode "text=$message" \
        --data-urlencode "parse_mode=Markdown")
    [ $? -ne 0 ] && return 1
    echo "$response" | grep -q '"ok":true' && return 0
    return 1
}

telegram_get_chat_id() {
    local response
    response=$(_telegram_api_curl "getUpdates")
    [ -z "$response" ] && return 1
    echo "$response" | grep -q '"ok":true' || return 1
    local chat_id=""
    if command -v python3 &>/dev/null; then
        chat_id=$(python3 -c "
import json,sys
try:
    d=json.loads(sys.stdin.read())
    msgs=d.get('result',[])
    if msgs:
        print(msgs[-1]['message']['chat']['id'])
except: pass
" <<< "$response" 2>/dev/null)
    fi
    if [ -z "$chat_id" ]; then
        chat_id=$(echo "$response" | grep -o '"chat"[[:space:]]*:[[:space:]]*{[[:space:]]*"id"[[:space:]]*:[[:space:]]*-\?[0-9]\+' | grep -o -- '-\?[0-9]\+$' | tail -1 2>/dev/null)
    fi
    if [ -n "$chat_id" ] && echo "$chat_id" | grep -qE '^-?[0-9]+$'; then
        TELEGRAM_CHAT_ID="$chat_id"
        return 0
    fi
    return 1
}

telegram_build_report() {
    local report="📊 *Paqet Status Report*"
    report+=$'\n'
    report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')"
    report+=$'\n\n'

    if is_running; then
        report+="✅ Status: Running"
    else
        report+="❌ Status: Stopped"
    fi
    report+=$'\n'
    report+="📡 Role: ${ROLE}"
    report+=$'\n'
    report+="📦 Version: ${PAQET_VERSION}"
    report+=$'\n'

    if [ "$ROLE" = "server" ]; then
        report+="🔌 Port: ${LISTEN_PORT}"
        report+=$'\n'
        if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
            report+="🛡 Firewall: Rules active"
        else
            report+="⚠️ Firewall: Rules missing"
        fi
    else
        report+="🔗 Server: ${REMOTE_SERVER}"
        report+=$'\n'
        report+="🧦 SOCKS: port ${SOCKS_PORT}"
    fi
    report+=$'\n'

    # Uptime
    if is_running && command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        local started
        started=$(systemctl show paqctl.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2)
        if [ -n "$started" ]; then
            local started_ts
            started_ts=$(date -d "$started" +%s 2>/dev/null || echo 0)
            if [ "$started_ts" -gt 0 ] 2>/dev/null; then
                local now=$(date +%s)
                local up=$((now - started_ts))
                local days=$((up / 86400))
                local hours=$(( (up % 86400) / 3600 ))
                local mins=$(( (up % 3600) / 60 ))
                if [ "$days" -gt 0 ]; then
                    report+="⏱ Uptime: ${days}d ${hours}h ${mins}m"
                else
                    report+="⏱ Uptime: ${hours}h ${mins}m"
                fi
                report+=$'\n'
            fi
        fi
    fi

    # CPU/RAM
    local pid
    if [ "$BACKEND" = "gfw-knocker" ]; then
        pid=$(pgrep -f "mainserver.py|mainclient.py" 2>/dev/null | head -1)
    else
        pid=$(pgrep -f "paqet run -c" 2>/dev/null | head -1)
    fi
    if [ -n "$pid" ]; then
        local cpu_mem
        cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1)
        if [ -n "$cpu_mem" ]; then
            local cpu=$(echo "$cpu_mem" | awk '{print $1}')
            local mem=$(echo "$cpu_mem" | awk '{print $2}')
            report+="💻 CPU: ${cpu}% | RAM: ${mem}%"
            report+=$'\n'
        fi
    fi

    echo "$report"
}

telegram_test_message() {
    local interval_label="${TELEGRAM_INTERVAL:-6}"
    local report=$(telegram_build_report)
    local backend_name="${BACKEND:-paqet}"

    # Backend-specific description
    local tech_desc=""
    if [ "$BACKEND" = "gfw-knocker" ]; then
        tech_desc="🔗 *What is GFW-Knocker?*
An advanced anti-censorship tool using 'violated TCP' packets + QUIC tunneling.
Designed for heavy DPI environments like the Great Firewall.
• Raw socket layer bypasses kernel TCP stack
• QUIC tunnel provides encrypted transport
• Requires Xray on server for SOCKS5 proxy"
    else
        tech_desc="🔗 *What is Paqet?*
A raw-socket encrypted proxy using KCP protocol.
Simple all-in-one solution with built-in SOCKS5 proxy.
• KCP over raw TCP packets with custom flags bypasses DPI
• Built-in SOCKS5 proxy (no extra software needed)
• Easy setup with just IP, port, and key"
    fi

    local message="✅ *paqctl Connected!*

📦 *About paqctl*
A unified management tool for bypass proxies.
Supports two backends for different network conditions:
• *paqet* — Simple KCP-based proxy (recommended)
• *gfw-knocker* — Advanced violated-TCP + QUIC tunnel

━━━━━━━━━━━━━━━━━━━━
${tech_desc}

📬 *What this bot sends you every ${interval_label}h:*
• Service status & uptime
• CPU & RAM usage
• Configuration summary
• Firewall rule status

⚠️ *Alerts:*
If the service goes down or is restarted, you will receive an immediate alert.

━━━━━━━━━━━━━━━━━━━━
🎮 *Available Commands:*
━━━━━━━━━━━━━━━━━━━━
/status — Full status report
/health — Run health check
/restart — Restart ${backend_name}
/stop — Stop ${backend_name}
/start — Start ${backend_name}
/version — Show version info

━━━━━━━━━━━━━━━━━━━━
📊 *Your first report:*
━━━━━━━━━━━━━━━━━━━━

${report}"
    telegram_send_message "$message"
}

telegram_generate_notify_script() {
    local script_path="$INSTALL_DIR/paqctl-telegram.sh"
    local _tmp
    _tmp=$(mktemp "${script_path}.XXXXXXXX")
    cat > "$_tmp" << 'TGSCRIPT'
#!/bin/bash
# paqctl Telegram notification daemon

INSTALL_DIR="REPLACE_ME_INSTALL_DIR"

# Safe settings loader - parses key=value with validation
_load_settings() {
    [ -f "$INSTALL_DIR/settings.conf" ] || return 0
    while IFS='=' read -r key value; do
        [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
        value="${value#\"}"; value="${value%\"}"
        # Skip values with dangerous shell characters
        [[ "$value" =~ [\`\$\(] ]] && continue
        case "$key" in
            BACKEND|ROLE|PAQET_VERSION|PAQCTL_VERSION|INTERFACE|LOCAL_IP|GATEWAY_MAC|\
            ENCRYPTION_KEY|REMOTE_SERVER|GFK_AUTH_CODE|GFK_PORT_MAPPINGS|GFK_SERVER_IP|\
            XRAY_PANEL_DETECTED|\
            TELEGRAM_BOT_TOKEN|TELEGRAM_CHAT_ID|TELEGRAM_SERVER_LABEL|\
            TELEGRAM_ENABLED|TELEGRAM_ALERTS_ENABLED|TELEGRAM_DAILY_SUMMARY|TELEGRAM_WEEKLY_SUMMARY)
                export "$key=$value" ;;
            LISTEN_PORT|SOCKS_PORT|GFK_VIO_PORT|GFK_VIO_CLIENT_PORT|GFK_QUIC_PORT|GFK_QUIC_CLIENT_PORT|MICROSOCKS_PORT|\
            GFK_SOCKS_PORT|GFK_SOCKS_VIO_PORT|\
            TELEGRAM_INTERVAL|TELEGRAM_START_HOUR)
                [[ "$value" =~ ^[0-9]*$ ]] && export "$key=$value" ;;
        esac
    done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
}
_load_settings

TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
TELEGRAM_INTERVAL=${TELEGRAM_INTERVAL:-6}
TELEGRAM_ALERTS_ENABLED=${TELEGRAM_ALERTS_ENABLED:-true}
TELEGRAM_DAILY_SUMMARY=${TELEGRAM_DAILY_SUMMARY:-true}
TELEGRAM_WEEKLY_SUMMARY=${TELEGRAM_WEEKLY_SUMMARY:-true}
TELEGRAM_START_HOUR=${TELEGRAM_START_HOUR:-0}

{ [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && exit 1

escape_telegram_markdown() {
    local text="$1"
    text="${text//\\/\\\\}"
    text="${text//\*/\\*}"
    text="${text//_/\\_}"
    text="${text//\`/\\\`}"
    text="${text//\[/\\[}"
    text="${text//\]/\\]}"
    echo "$text"
}

# Secure Telegram API curl - writes token to temp file to avoid /proc exposure
_tg_api_curl() {
    local endpoint="$1"
    shift
    local _tg_tmp
    _tg_tmp=$(mktemp "${INSTALL_DIR}/.tg_curl.XXXXXXXX") || return 1
    chmod 600 "$_tg_tmp" 2>/dev/null
    printf 'url = "https://api.telegram.org/bot%s/%s"\n' "$TELEGRAM_BOT_TOKEN" "$endpoint" > "$_tg_tmp"
    local _result
    _result=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_tg_tmp" "$@" 2>/dev/null)
    local _exit=$?
    rm -f "$_tg_tmp"
    [ $_exit -eq 0 ] && echo "$_result"
    return $_exit
}

send_message() {
    local message="$1"
    { [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && return 1
    local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
    label=$(escape_telegram_markdown "$label")
    local _ip=$(curl -s --max-time 3 https://api.ipify.org 2>/dev/null || echo "")
    [ -n "$_ip" ] && message="[${label} | ${_ip}] ${message}" || message="[${label}] ${message}"
    local response
    response=$(_tg_api_curl "sendMessage" \
        -X POST \
        --data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
        --data-urlencode "text=$message" \
        --data-urlencode "parse_mode=Markdown")
    [ $? -ne 0 ] && return 1
    echo "$response" | grep -q '"ok":true' && return 0
    return 1
}

is_running() {
    if [ "$BACKEND" = "gfw-knocker" ]; then
        pgrep -f "mainserver.py|mainclient.py|gfk-client.sh" &>/dev/null
    else
        pgrep -f "paqet run -c" &>/dev/null
    fi
}

get_main_pid() {
    if [ "$BACKEND" = "gfw-knocker" ]; then
        pgrep -f "mainserver.py" 2>/dev/null | head -1
    else
        pgrep -f "paqet run -c" 2>/dev/null | head -1
    fi
}

build_report() {
    local report="📊 *${BACKEND} Status Report*"$'\n'
    report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')"$'\n\n'
    if is_running; then
        report+="✅ Status: Running"
    else
        report+="❌ Status: Stopped"
    fi
    report+=$'\n'"📡 Role: ${ROLE:-unknown}"$'\n'
    report+="📦 Version: ${PAQET_VERSION:-unknown}"$'\n'
    local pid=$(get_main_pid)
    if [ -n "$pid" ]; then
        local cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1)
        if [ -n "$cpu_mem" ]; then
            local cpu=$(echo "$cpu_mem" | awk '{print $1}')
            local mem=$(echo "$cpu_mem" | awk '{print $2}')
            report+="💻 CPU: ${cpu}% | RAM: ${mem}%"$'\n'
        fi
    fi
    echo "$report"
}

LAST_COMMAND_TIME=0
COMMAND_COOLDOWN=5

check_commands() {
    local response
    response=$(_tg_api_curl "getUpdates" \
        -X POST \
        --data-urlencode "offset=${LAST_UPDATE_ID:-0}" \
        --data-urlencode "limit=10")
    [ -z "$response" ] && return
    echo "$response" | grep -q '"ok":true' || return

    if command -v python3 &>/dev/null; then
        local cmds
        local _safe_chat_id
        _safe_chat_id=$(printf '%s' "$TELEGRAM_CHAT_ID" | tr -cd '0-9-')
        [ -z "$_safe_chat_id" ] && return
        cmds=$(python3 -c "
import json,sys
try:
    d=json.loads(sys.stdin.read())
    chat_id=sys.argv[1]
    if not chat_id: sys.exit(0)
    for r in d.get('result',[]):
        uid=r['update_id']
        txt=r.get('message',{}).get('text','').replace('|','')
        cid=str(r.get('message',{}).get('chat',{}).get('id',''))
        if cid==chat_id and txt.startswith('/'):
            print(f'{uid}|{txt}')
except: pass
" "$_safe_chat_id" <<< "$response" 2>/dev/null)

        while IFS='|' read -r uid cmd; do
            [ -z "$uid" ] && continue
            # Validate uid is numeric
            [[ "$uid" =~ ^[0-9]+$ ]] || continue
            LAST_UPDATE_ID=$((uid + 1))
            cmd="${cmd%% *}"  # strip arguments, match command only

            # Rate limiting
            local _now
            _now=$(date +%s)
            if [ $((_now - LAST_COMMAND_TIME)) -lt $COMMAND_COOLDOWN ]; then
                continue
            fi
            LAST_COMMAND_TIME=$_now

            case "$cmd" in
                /status)  send_message "$(build_report)" ;;
                /health)  send_message "$(/usr/local/bin/paqctl health 2>&1 | head -30)" ;;
                /restart) /usr/local/bin/paqctl restart 2>&1; send_message "🔄 Service restarted" ;;
                /stop)    /usr/local/bin/paqctl stop 2>&1; send_message "⏹ Service stopped" ;;
                /start)   /usr/local/bin/paqctl start 2>&1; send_message "▶️ Service started" ;;
                /version) send_message "📦 Version: ${PAQET_VERSION:-unknown} | paqctl: ${PAQCTL_VERSION:-unknown}" ;;
            esac
        done <<< "$cmds"
    fi
}

# Alert state
LAST_STATE="unknown"
LAST_REPORT=0
LAST_DAILY=0
LAST_WEEKLY=0
LAST_UPDATE_ID=0

# Initialize update offset
init_response=$(_tg_api_curl "getUpdates" \
    -X POST \
    --data-urlencode "offset=-1")
if command -v python3 &>/dev/null; then
    LAST_UPDATE_ID=$(python3 -c "
import json,sys
try:
    d=json.loads(sys.stdin.read())
    r=d.get('result',[])
    if r: print(r[-1]['update_id']+1)
    else: print(0)
except: print(0)
" <<< "$init_response" 2>/dev/null)
fi
LAST_UPDATE_ID=${LAST_UPDATE_ID:-0}

# Send startup notification
send_message "🚀 *Telegram notifications started*"$'\n'"Reports every ${TELEGRAM_INTERVAL}h | Alerts: ${TELEGRAM_ALERTS_ENABLED}"

while true; do
    # Reload settings periodically (safe parser, no code execution)
    _load_settings

    # Check commands from Telegram
    check_commands

    # Service state alerts
    current_state="stopped"
    is_running && current_state="running"

    if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ]; then
        if [ "$LAST_STATE" = "running" ] && [ "$current_state" = "stopped" ]; then
            send_message "🚨 *ALERT:* ${BACKEND} service has stopped!"
        elif [ "$LAST_STATE" = "stopped" ] && [ "$current_state" = "running" ]; then
            send_message "✅ ${BACKEND} service is back up"
        fi

        # High CPU alert
        _pid=$(get_main_pid)
        if [ -n "$_pid" ]; then
            _cpu=$(ps -p "$_pid" -o %cpu= 2>/dev/null | awk '{printf "%.0f", $1}')
            if [ "${_cpu:-0}" -gt 80 ] 2>/dev/null; then
                send_message "⚠️ High CPU usage: ${_cpu}%"
            fi
        fi
    fi
    LAST_STATE="$current_state"

    # Periodic reports
    _now=$(date +%s)
    _interval_secs=$(( ${TELEGRAM_INTERVAL:-6} * 3600 ))
    if [ $((_now - LAST_REPORT)) -ge "$_interval_secs" ]; then
        send_message "$(build_report)"
        LAST_REPORT=$_now
    fi

    # Daily summary
    _hour=$(date +%H)
    _day_of_week=$(date +%u)
    if [ "$TELEGRAM_DAILY_SUMMARY" = "true" ] && [ "$_hour" = "$(printf '%02d' ${TELEGRAM_START_HOUR:-0})" ]; then
        if [ $((_now - LAST_DAILY)) -ge 86400 ]; then
            send_message "📅 *Daily Summary*"$'\n'"$(build_report)"
            LAST_DAILY=$_now
        fi
    fi

    # Weekly summary (Monday)
    if [ "$TELEGRAM_WEEKLY_SUMMARY" = "true" ] && [ "$_day_of_week" = "1" ] && [ "$_hour" = "$(printf '%02d' ${TELEGRAM_START_HOUR:-0})" ]; then
        if [ $((_now - LAST_WEEKLY)) -ge 604800 ]; then
            send_message "📆 *Weekly Summary*"$'\n'"$(build_report)"
            LAST_WEEKLY=$_now
        fi
    fi

    sleep 30
done
TGSCRIPT

    sed "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$_tmp" > "$_tmp.sed" && mv "$_tmp.sed" "$_tmp"
    if ! chmod +x "$_tmp"; then
        log_error "Failed to make Telegram script executable"
        rm -f "$_tmp"
        return 1
    fi
    if ! mv "$_tmp" "$script_path"; then
        log_error "Failed to install Telegram script"
        rm -f "$_tmp"
        return 1
    fi
}

setup_telegram_service() {
    telegram_generate_notify_script

    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        cat > /etc/systemd/system/paqctl-telegram.service << EOF
[Unit]
Description=paqctl Telegram Notification Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=$(command -v bash) ${INSTALL_DIR}/paqctl-telegram.sh
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF
        systemctl daemon-reload 2>/dev/null || true
        systemctl enable paqctl-telegram.service 2>/dev/null || true
        systemctl start paqctl-telegram.service 2>/dev/null || true
        log_success "Telegram service started"
    else
        log_warn "Systemd not available. Run the Telegram daemon manually:"
        log_info "  nohup bash $INSTALL_DIR/paqctl-telegram.sh &"
    fi
}

stop_telegram_service() {
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl stop paqctl-telegram.service 2>/dev/null || true
        systemctl disable paqctl-telegram.service 2>/dev/null || true
    fi
    pkill -f "paqctl-telegram.sh" 2>/dev/null || true
    log_success "Telegram service stopped"
}

show_telegram_menu() {
    local redraw=true
    while true; do
        if [ "$redraw" = true ]; then
            clear
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo -e "${BOLD}  TELEGRAM NOTIFICATIONS${NC}"
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo ""

            if [ "$TELEGRAM_ENABLED" = "true" ]; then
                echo -e "  Status: ${GREEN}Enabled${NC}"
                if command -v systemctl &>/dev/null && systemctl is-active paqctl-telegram.service &>/dev/null; then
                    echo -e "  Service: ${GREEN}Running${NC}"
                else
                    echo -e "  Service: ${RED}Stopped${NC}"
                fi
            else
                echo -e "  Status: ${DIM}Disabled${NC}"
            fi

            echo ""
            echo "  1. Setup / Change bot"
            echo "  2. Test notification"
            echo "  3. Enable & start service"
            echo "  4. Disable & stop service"
            echo "  5. Set check interval (currently: ${TELEGRAM_INTERVAL}h)"
            echo "  6. Set server label (currently: ${TELEGRAM_SERVER_LABEL:-hostname})"
            echo "  7. Toggle alerts (currently: ${TELEGRAM_ALERTS_ENABLED})"
            echo "  b. Back"
            echo ""
            redraw=false
        fi

        read -p "  Choice: " tg_choice < /dev/tty || break
        case "$tg_choice" in
            1)
                echo ""
                echo -e "${BOLD}Telegram Bot Setup${NC}"
                echo ""
                echo "  1. Open Telegram and message @BotFather"
                echo "  2. Send /newbot and follow the steps"
                echo "  3. Copy the bot token"
                echo ""
                read -p "  Enter bot token: " input < /dev/tty || true
                if [ -n "$input" ]; then
                    TELEGRAM_BOT_TOKEN="$input"
                    echo ""
                    echo "  Now send any message to your bot in Telegram..."
                    echo ""
                    for _i in $(seq 15 -1 1); do
                        printf "\r  Waiting: %2ds " "$_i"
                        sleep 1
                    done
                    printf "\r                    \r"
                    if telegram_get_chat_id; then
                        log_success "Chat ID detected: $TELEGRAM_CHAT_ID"
                        # Save
                        _safe_update_setting "TELEGRAM_BOT_TOKEN" "$TELEGRAM_BOT_TOKEN" "$INSTALL_DIR/settings.conf"
                        _safe_update_setting "TELEGRAM_CHAT_ID" "$TELEGRAM_CHAT_ID" "$INSTALL_DIR/settings.conf"
                    else
                        log_error "Could not detect chat ID. Make sure you sent a message to the bot."
                        echo ""
                        read -p "  Enter chat ID manually (or press Enter to cancel): " input < /dev/tty || true
                        if [ -n "$input" ]; then
                            TELEGRAM_CHAT_ID="$input"
                            _safe_update_setting "TELEGRAM_BOT_TOKEN" "$TELEGRAM_BOT_TOKEN" "$INSTALL_DIR/settings.conf"
                            _safe_update_setting "TELEGRAM_CHAT_ID" "$TELEGRAM_CHAT_ID" "$INSTALL_DIR/settings.conf"
                        fi
                    fi
                fi
                redraw=true
                ;;
            2)
                if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
                    log_error "Bot not configured. Run setup first."
                else
                    if telegram_test_message; then
                        log_success "Test message sent!"
                    else
                        log_error "Failed to send. Check token and chat ID."
                    fi
                fi
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            3)
                if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
                    log_error "Bot not configured. Run setup first."
                else
                    TELEGRAM_ENABLED=true
                    _safe_update_setting "TELEGRAM_ENABLED" "true" "$INSTALL_DIR/settings.conf"
                    setup_telegram_service
                fi
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            4)
                TELEGRAM_ENABLED=false
                _safe_update_setting "TELEGRAM_ENABLED" "false" "$INSTALL_DIR/settings.conf"
                stop_telegram_service
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            5)
                echo ""
                read -p "  Check interval in hours [1-24]: " input < /dev/tty || true
                if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le 24 ]; then
                    TELEGRAM_INTERVAL="$input"
                    _safe_update_setting "TELEGRAM_INTERVAL" "$input" "$INSTALL_DIR/settings.conf"
                    log_success "Interval set to ${input}h"
                    # Restart service if running
                    if command -v systemctl &>/dev/null && systemctl is-active paqctl-telegram.service &>/dev/null; then
                        telegram_generate_notify_script
                        systemctl restart paqctl-telegram.service 2>/dev/null || true
                    fi
                else
                    log_warn "Invalid value"
                fi
                redraw=true
                ;;
            6)
                echo ""
                read -p "  Server label: " input < /dev/tty || true
                if [ -n "$input" ]; then
                    TELEGRAM_SERVER_LABEL="$input"
                    _safe_update_setting "TELEGRAM_SERVER_LABEL" "$input" "$INSTALL_DIR/settings.conf"
                    log_success "Label set to: $input"
                fi
                redraw=true
                ;;
            7)
                if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ]; then
                    TELEGRAM_ALERTS_ENABLED=false
                else
                    TELEGRAM_ALERTS_ENABLED=true
                fi
                _safe_update_setting "TELEGRAM_ALERTS_ENABLED" "$TELEGRAM_ALERTS_ENABLED" "$INSTALL_DIR/settings.conf"
                log_info "Alerts: $TELEGRAM_ALERTS_ENABLED"
                redraw=true
                ;;
            b|B) return ;;
            "") ;;
            *) echo -e "  ${RED}Invalid choice${NC}" ;;
        esac
    done
}

#═══════════════════════════════════════════════════════════════════════
# Switch Backend
#═══════════════════════════════════════════════════════════════════════

switch_backend() {
    local current_backend="${BACKEND:-paqet}"
    local new_backend
    if [ "$current_backend" = "paqet" ]; then
        new_backend="gfw-knocker"
    else
        new_backend="paqet"
    fi

    # Check if the other backend is installed
    local other_installed=false
    if [ "$new_backend" = "gfw-knocker" ]; then
        if [ "$ROLE" = "server" ]; then
            [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && other_installed=true
        else
            [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && other_installed=true
        fi
    else
        [ -f "$INSTALL_DIR/bin/paqet" ] && other_installed=true
    fi

    if [ "$other_installed" = false ]; then
        echo ""
        echo -e "${YELLOW}${new_backend} is not installed.${NC}"
        echo ""
        echo "  Use 'Install additional backend' option to install it first."
        echo ""
        read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
        return 0
    fi

    echo ""
    echo -e "${BOLD}Switch active backend from ${current_backend} to ${new_backend}?${NC}"
    echo ""
    echo "  This will:"
    echo "  - Stop ${current_backend}"
    echo "  - Start ${new_backend}"
    echo ""
    read -p "  Proceed? [y/N]: " confirm < /dev/tty || true
    [[ "$confirm" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; }

    # Stop current
    stop_paqet
    _remove_firewall

    # Switch to new backend
    BACKEND="$new_backend"
    save_settings

    # Setup firewall and start new backend
    _apply_firewall
    start_paqet

    log_success "Switched to ${new_backend}"
    read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
}

install_additional_backend() {
    local current_backend="${BACKEND:-paqet}"
    local new_backend
    if [ "$current_backend" = "paqet" ]; then
        new_backend="gfw-knocker"
    else
        new_backend="paqet"
    fi

    # Check if already installed
    local already_installed=false
    if [ "$new_backend" = "gfw-knocker" ]; then
        if [ "$ROLE" = "server" ]; then
            [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && already_installed=true
        else
            [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && already_installed=true
        fi
    else
        [ -f "$INSTALL_DIR/bin/paqet" ] && already_installed=true
    fi

    if [ "$already_installed" = true ]; then
        echo ""
        echo -e "${GREEN}${new_backend} is already installed.${NC}"
        echo ""
        echo "  Use 'Switch backend' to change the active backend."
        echo ""
        read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
        return 0
    fi

    echo ""
    echo -e "${BOLD}Install ${new_backend} alongside ${current_backend}?${NC}"
    echo ""
    echo "  This will:"
    echo "  - Keep ${current_backend} running"
    echo "  - Install ${new_backend} as an additional option"
    echo "  - You can switch between them anytime"
    echo ""
    read -p "  Proceed? [y/N]: " confirm < /dev/tty || true
    [[ "$confirm" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; }

    echo ""
    log_info "Installing ${new_backend}..."

    if [ "$new_backend" = "gfw-knocker" ]; then
        # Collect GFK configuration for client role
        if [ "$ROLE" = "client" ]; then
            echo ""
            echo -e "${BOLD}GFK Client Configuration${NC}"
            echo -e "${DIM}(these must match your server settings)${NC}"
            echo ""

            echo -e "${BOLD}Server IP${NC} (server's public IP):"
            read -p "  IP: " input < /dev/tty || true
            if [ -z "$input" ] || ! _validate_ip "$input"; then
                log_error "Valid server IP is required."
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                return 1
            fi
            GFK_SERVER_IP="$input"

            echo -e "${BOLD}Server's VIO TCP port${NC} [45000] (must match server):"
            read -p "  Port: " input < /dev/tty || true
            GFK_VIO_PORT="${input:-45000}"
            if ! _validate_port "$GFK_VIO_PORT"; then
                log_warn "Invalid port. Using default 45000."
                GFK_VIO_PORT=45000
            fi

            echo -e "${BOLD}Local VIO client port${NC} [40000]:"
            read -p "  Port: " input < /dev/tty || true
            GFK_VIO_CLIENT_PORT="${input:-40000}"
            if ! _validate_port "$GFK_VIO_CLIENT_PORT"; then
                log_warn "Invalid port. Using default 40000."
                GFK_VIO_CLIENT_PORT=40000
            fi

            echo -e "${BOLD}Server's QUIC port${NC} [25000] (must match server):"
            read -p "  Port: " input < /dev/tty || true
            GFK_QUIC_PORT="${input:-25000}"
            if ! _validate_port "$GFK_QUIC_PORT"; then
                log_warn "Invalid port. Using default 25000."
                GFK_QUIC_PORT=25000
            fi

            echo -e "${BOLD}Local QUIC client port${NC} [20000]:"
            read -p "  Port: " input < /dev/tty || true
            GFK_QUIC_CLIENT_PORT="${input:-20000}"
            if ! _validate_port "$GFK_QUIC_CLIENT_PORT"; then
                log_warn "Invalid port. Using default 20000."
                GFK_QUIC_CLIENT_PORT=20000
            fi

            echo -e "${BOLD}QUIC auth code${NC} (from server setup):"
            read -p "  Auth code: " input < /dev/tty || true
            if [ -z "$input" ]; then
                log_error "Auth code is required."
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                return 1
            fi
            GFK_AUTH_CODE="$input"

            echo -e "${BOLD}TCP port mappings${NC} (must match server) [14000:443]:"
            read -p "  Mappings: " input < /dev/tty || true
            GFK_PORT_MAPPINGS="${input:-14000:443}"
            echo ""
        fi

        # Install GFK without changing current backend
        if ! _install_gfk_components; then
            log_error "Failed to install ${new_backend}"
            read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
            return 1
        fi
    else
        # Install paqet without changing current backend
        if ! _install_paqet_components; then
            log_error "Failed to install ${new_backend}"
            read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
            return 1
        fi
    fi

    echo ""
    log_success "${new_backend} installed successfully!"
    echo ""
    echo "  Use 'Switch backend' to activate it."
    echo ""
    read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
}

_install_paqet_components() {
    log_info "Downloading paqet binary..."
    local _paqet_ver
    _paqet_ver=$(curl -s --max-time 10 "$PAQET_API_URL" 2>/dev/null | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
    if [ -z "$_paqet_ver" ] || ! _validate_version_tag "$_paqet_ver"; then
        _paqet_ver="$PAQET_VERSION_PINNED"
    fi
    log_info "Using paqet ${_paqet_ver}"
    if ! download_paqet "$_paqet_ver"; then
        log_error "Failed to download paqet"
        return 1
    fi
    log_success "paqet binary installed"

    # Generate config.yaml if it doesn't exist
    if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
        echo ""
        echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
        echo -e "${BOLD}  PAQET CONFIGURATION${NC}"
        echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
        echo ""

        # Detect network settings
        detect_network
        local _det_iface="$DETECTED_IFACE"
        local _det_ip="$DETECTED_IP"
        local _det_mac="$DETECTED_GW_MAC"

        # Prompt for interface
        echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:"
        read -p "  Interface: " input < /dev/tty || true
        local _iface="${input:-${_det_iface:-eth0}}"

        # Prompt for local IP
        echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:"
        read -p "  IP: " input < /dev/tty || true
        local _local_ip="${input:-$_det_ip}"

        # Prompt for gateway MAC
        echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:"
        read -p "  MAC: " input < /dev/tty || true
        local _gw_mac="${input:-$_det_mac}"

        # Validate MAC if provided
        if [ -n "$_gw_mac" ] && ! [[ "$_gw_mac" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; then
            log_warn "Invalid MAC format. Expected: aa:bb:cc:dd:ee:ff"
            read -p "  Enter valid MAC: " input < /dev/tty || true
            [ -n "$input" ] && _gw_mac="$input"
        fi

        # Generate encryption key
        local _key
        _key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
        if [ -z "$_key" ]; then
            _key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
        fi

        if [ "$ROLE" = "server" ]; then
            # Prompt for port
            echo -e "${BOLD}Listen Port${NC} [8443]:"
            read -p "  Port: " input < /dev/tty || true
            local _port="${input:-8443}"

            # Show generated key
            echo ""
            echo -e "${GREEN}${BOLD}  Generated Encryption Key: ${_key}${NC}"
            echo -e "${YELLOW}  IMPORTANT: Save this key! Clients need it to connect.${NC}"
            echo ""
            echo -e "${BOLD}Encryption Key${NC} (press Enter to use generated key):"
            read -p "  Key: " input < /dev/tty || true
            [ -n "$input" ] && _key="$input"

            LISTEN_PORT="$_port"
            ENCRYPTION_KEY="$_key"
        else
            # Client prompts
            echo -e "${BOLD}Remote Server${NC} (IP:PORT):"
            read -p "  Server: " input < /dev/tty || true
            local _server="${input:-${REMOTE_SERVER:-}}"
            if [ -z "$_server" ]; then
                log_warn "No server specified. You must edit config.yaml later."
                _server="SERVER_IP:8443"
            fi

            echo -e "${BOLD}Encryption Key${NC} (from server):"
            read -p "  Key: " input < /dev/tty || true
            [ -n "$input" ] && _key="$input"

            echo -e "${BOLD}SOCKS5 Port${NC} [1080]:"
            read -p "  Port: " input < /dev/tty || true
            local _socks="${input:-1080}"

            REMOTE_SERVER="$_server"
            SOCKS_PORT="$_socks"
            ENCRYPTION_KEY="$_key"
        fi

        # Validate required fields
        if [ -z "$_iface" ] || [ -z "$_local_ip" ] || [ -z "$_gw_mac" ]; then
            log_error "Missing required fields (interface, IP, or MAC)"
            return 1
        fi
        if [ -z "$_key" ] || [ "${#_key}" -lt 16 ]; then
            log_error "Invalid encryption key"
            return 1
        fi

        # Helper to escape YAML values
        _escape_yaml_val() {
            local s="$1"
            if [[ "$s" =~ [:\#\[\]{}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
                s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s"
            else
                printf '%s' "$s"
            fi
        }

        local _y_iface _y_ip _y_mac _y_key
        _y_iface=$(_escape_yaml_val "$_iface")
        _y_ip=$(_escape_yaml_val "$_local_ip")
        _y_mac=$(_escape_yaml_val "$_gw_mac")
        _y_key=$(_escape_yaml_val "$_key")

        local tmp_conf
        tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
        chmod 600 "$tmp_conf" 2>/dev/null

        if [ "$ROLE" = "server" ]; then
            cat > "$tmp_conf" << EOF
role: "server"

log:
  level: "info"

listen:
  addr: ":${_port}"

network:
  interface: "${_y_iface}"
  ipv4:
    addr: "${_y_ip}:${_port}"
    router_mac: "${_y_mac}"

transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_y_key}"
EOF
        else
            cat > "$tmp_conf" << EOF
role: "client"

log:
  level: "info"

socks5:
  - listen: "127.0.0.1:${_socks}"

network:
  interface: "${_y_iface}"
  ipv4:
    addr: "${_y_ip}:0"
    router_mac: "${_y_mac}"

server:
  addr: "${_server}"

transport:
  protocol: "kcp"
  kcp:
    mode: "fast"
    key: "${_y_key}"
EOF
        fi

        if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
            log_error "Failed to save config.yaml"
            rm -f "$tmp_conf"
            return 1
        fi
        chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null

        # Update global vars for settings
        INTERFACE="$_iface"
        LOCAL_IP="$_local_ip"
        GATEWAY_MAC="$_gw_mac"

        log_success "Configuration saved to $INSTALL_DIR/config.yaml"

        # Save to settings.conf for persistence
        save_settings 2>/dev/null || true
    fi
}

check_xray_installed() {
    command -v xray &>/dev/null && return 0
    [ -x /usr/local/bin/xray ] && return 0
    [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && return 0
    return 1
}

XRAY_CONFIG_DIR="/usr/local/etc/xray"
XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json"

install_xray() {
    if check_xray_installed; then
        log_info "Xray is already installed"
        return 0
    fi

    log_info "Installing Xray ${XRAY_VERSION_PINNED}..."

    local tmp_script
    tmp_script=$(mktemp)
    if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then
        log_error "Failed to download Xray installer"
        rm -f "$tmp_script"
        return 1
    fi

    if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then
        log_error "Failed to install Xray"
        rm -f "$tmp_script"
        return 1
    fi
    rm -f "$tmp_script"

    log_success "Xray ${XRAY_VERSION_PINNED} installed"
}

configure_xray_socks() {
    local listen_port="${1:-443}"
    log_info "Configuring Xray SOCKS5 proxy on port $listen_port..."
    mkdir -p "$XRAY_CONFIG_DIR"
    cat > "$XRAY_CONFIG_FILE" << EOF
{
  "log": { "loglevel": "warning" },
  "inbounds": [{
    "tag": "socks-in",
    "port": ${listen_port},
    "listen": "127.0.0.1",
    "protocol": "socks",
    "settings": { "auth": "noauth", "udp": true },
    "sniffing": { "enabled": true, "destOverride": ["http", "tls"] }
  }],
  "outbounds": [{ "tag": "direct", "protocol": "freedom", "settings": {} }]
}
EOF
    chmod 644 "$XRAY_CONFIG_FILE"
    log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)"
}

_is_paqctl_standalone_xray() {
    [ -f "$XRAY_CONFIG_FILE" ] || return 1
    command -v python3 &>/dev/null || return 1
    python3 -c "
import json, sys
try:
    with open(sys.argv[1]) as f:
        cfg = json.load(f)
    inbounds = cfg.get('inbounds', [])
    if not inbounds:
        sys.exit(1)
    for i in inbounds:
        if i.get('protocol') != 'socks' or i.get('listen', '0.0.0.0') != '127.0.0.1':
            sys.exit(1)
    sys.exit(0)
except:
    sys.exit(1)
" "$XRAY_CONFIG_FILE" 2>/dev/null
}

_add_xray_gfk_socks() {
    local port="$1"
    python3 -c "
import json, sys
port = int(sys.argv[1])
config_path = sys.argv[2]
try:
    with open(config_path, 'r') as f:
        cfg = json.load(f)
except:
    cfg = {'inbounds': [], 'outbounds': [{'tag': 'direct', 'protocol': 'freedom', 'settings': {}}]}
cfg.setdefault('inbounds', [])
cfg['inbounds'] = [i for i in cfg['inbounds'] if i.get('tag') != 'gfk-socks']
cfg['inbounds'].append({
    'tag': 'gfk-socks', 'port': port, 'listen': '127.0.0.1', 'protocol': 'socks',
    'settings': {'auth': 'noauth', 'udp': True},
    'sniffing': {'enabled': True, 'destOverride': ['http', 'tls']}
})
if not any(o.get('protocol') == 'freedom' for o in cfg.get('outbounds', [])):
    cfg.setdefault('outbounds', []).append({'tag': 'direct', 'protocol': 'freedom', 'settings': {}})
with open(config_path, 'w') as f:
    json.dump(cfg, f, indent=2)
" "$port" "$XRAY_CONFIG_FILE" 2>/dev/null
    if [ $? -ne 0 ]; then
        log_error "Failed to add SOCKS5 inbound to existing Xray config"
        return 1
    fi
    log_success "Added GFK SOCKS5 inbound on 127.0.0.1:$port"
}

stop_xray() {
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl stop xray 2>/dev/null || true
    else
        pkill -x xray 2>/dev/null || true
    fi
}

start_xray() {
    log_info "Starting Xray service..."
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl stop xray 2>/dev/null || true
        sleep 1
        systemctl daemon-reload 2>/dev/null || true
        systemctl enable xray 2>/dev/null || true
        local attempt
        for attempt in 1 2 3; do
            systemctl start xray 2>/dev/null
            sleep 2
            if systemctl is-active --quiet xray; then
                log_success "Xray started"
                return 0
            fi
            [ "$attempt" -lt 3 ] && sleep 1
        done
        log_error "Failed to start Xray after 3 attempts"
        return 1
    else
        local _xray_bin=""
        [ -x /usr/local/bin/xray ] && _xray_bin="/usr/local/bin/xray"
        [ -z "$_xray_bin" ] && [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && _xray_bin="/usr/local/x-ui/bin/xray-linux-amd64"
        if [ -n "$_xray_bin" ]; then
            pkill -x xray 2>/dev/null || true
            sleep 1
            nohup "$_xray_bin" run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
            sleep 2
            if pgrep -f "xray" &>/dev/null; then
                log_success "Xray started"
                return 0
            fi
        fi
        log_error "Failed to start Xray"
        return 1
    fi
}

setup_xray_for_gfk() {
    local target_port
    target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)

    if pgrep -x xray &>/dev/null || pgrep -f "xray-linux" &>/dev/null; then
        # Check if this is paqctl's own standalone Xray (not a real panel)
        if _is_paqctl_standalone_xray; then
            log_info "Existing Xray is paqctl's standalone install — reconfiguring..."
            stop_xray
            sleep 1
            # Fall through to standalone install path below
        else
            XRAY_PANEL_DETECTED=true
            log_info "Existing Xray detected — adding SOCKS5 alongside panel..."

            # Clean up any leftover standalone GFK xray from prior installs
            pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true
            rm -f "${XRAY_CONFIG_DIR}/gfk-socks.json" 2>/dev/null

            # Check all existing target ports from mappings
            local mapping pairs
            IFS=',' read -ra pairs <<< "${GFK_PORT_MAPPINGS:-14000:443}"
            for mapping in "${pairs[@]}"; do
                local vio_port="${mapping%%:*}"
                local tp="${mapping##*:}"
                if ss -tln 2>/dev/null | grep -q ":${tp} "; then
                    log_success "Port $tp is listening — GFK will forward VIO port $vio_port to this port"
                else
                    log_warn "Port $tp is NOT listening — make sure your panel inbound is on port $tp"
                fi
            done

            # Find free port for SOCKS5 (starting at 10443)
            local socks_port=10443
            while ss -tln 2>/dev/null | grep -q ":${socks_port} "; do
                socks_port=$((socks_port + 1))
                if [ "$socks_port" -gt 65000 ]; then
                    log_warn "Could not find free port for SOCKS5 — panel-only mode"
                    echo ""
                    local first_vio
                    first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
                    log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
                    return 0
                fi
            done

            # Add SOCKS5 inbound to existing xray config
            _add_xray_gfk_socks "$socks_port" || {
                log_warn "Could not add SOCKS5 to panel config — panel-only mode"
                return 0
            }

            # Restart xray to load new config
            systemctl restart xray 2>/dev/null || pkill -SIGHUP xray 2>/dev/null || true
            sleep 2

            # Find next VIO port (highest existing + 1) and append SOCKS5 mapping
            local max_vio=0
            for mapping in "${pairs[@]}"; do
                local v="${mapping%%:*}"
                [ "$v" -gt "$max_vio" ] && max_vio="$v"
            done
            local socks_vio=$((max_vio + 1))
            GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS},${socks_vio}:${socks_port}"
            GFK_SOCKS_PORT="$socks_port"
            GFK_SOCKS_VIO_PORT="$socks_vio"

            log_success "SOCKS5 proxy added on port $socks_port (VIO port $socks_vio)"
            echo ""
            log_info "Port mappings updated: ${GFK_PORT_MAPPINGS}"
            log_warn "Use these SAME mappings on the client side"
            echo ""
            local first_vio
            first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
            log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
            log_warn "For direct SOCKS5: use 127.0.0.1:${socks_vio} as your proxy on client"
            return 0
        fi
    fi

    install_xray || return 1
    configure_xray_socks "$target_port" || return 1
    start_xray || return 1
}

_install_gfk_components() {
    log_info "Installing GFK components..."

    # Auto-detect server IP if not set (critical for server-side sniffer filter)
    if [ -z "${GFK_SERVER_IP:-}" ] && [ "$ROLE" = "server" ]; then
        GFK_SERVER_IP="${LOCAL_IP:-}"
        [ -z "$GFK_SERVER_IP" ] && GFK_SERVER_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
        [ -z "$GFK_SERVER_IP" ] && GFK_SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
        if [ -n "$GFK_SERVER_IP" ]; then
            log_info "Auto-detected server IP: ${GFK_SERVER_IP}"
        else
            log_error "Could not detect server IP. Set GFK_SERVER_IP manually."
            return 1
        fi
    fi

    # Auto-generate auth code if not set
    if [ -z "${GFK_AUTH_CODE:-}" ] || [ "$GFK_AUTH_CODE" = "not set" ]; then
        GFK_AUTH_CODE=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 16 2>/dev/null || openssl rand -hex 8)
        log_info "Generated GFK auth code: ${GFK_AUTH_CODE}"
    fi

    # Save settings with server IP and auth code
    save_settings

    # Install Python dependencies (venv + scapy + aioquic)
    install_python_deps || return 1

    # Download GFK scripts (server and client)
    download_gfk || return 1

    # Generate TLS certificates for QUIC
    generate_gfk_certs || return 1

    # Setup Xray (server only — adds SOCKS5 alongside panel if detected)
    if [ "$ROLE" = "server" ]; then
        setup_xray_for_gfk || return 1
    elif [ "$ROLE" = "client" ]; then
        create_gfk_client_wrapper
    fi

    # Generate parameters.py config
    generate_gfk_config || return 1

    save_settings

    log_success "GFK components installed"
}

#═══════════════════════════════════════════════════════════════════════
# Uninstall
#═══════════════════════════════════════════════════════════════════════

uninstall_paqctl() {
    echo ""
    echo -e "${RED}${BOLD}  UNINSTALL PAQCTL${NC}"
    echo ""
    echo -e "  This will remove:"
    if [ "$BACKEND" = "gfw-knocker" ]; then
        echo "  - GFW-knocker scripts and config"
    else
        echo "  - paqet binary"
    fi
    echo "  - All configuration files"
    echo "  - Systemd services"
    echo "  - Firewall rules"
    echo "  - Telegram service"
    echo ""
    read -p "  Are you sure? [y/N]: " confirm < /dev/tty || true
    if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
        log_info "Cancelled"
        return 0
    fi

    # Stop services
    stop_paqet
    stop_telegram_service

    # Stop standalone GFK xray and clean up config
    pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true
    rm -f /usr/local/etc/xray/gfk-socks.json 2>/dev/null
    # If xray is paqctl's standalone install, stop and disable it entirely
    if _is_paqctl_standalone_xray; then
        log_info "Stopping paqctl's standalone Xray..."
        systemctl stop xray 2>/dev/null || true
        systemctl disable xray 2>/dev/null || true
    elif [ -f "$XRAY_CONFIG_FILE" ] && command -v python3 &>/dev/null; then
        # Remove gfk-socks inbound from panel's xray config if present
        python3 -c "
import json, sys
try:
    with open(sys.argv[1], 'r') as f:
        cfg = json.load(f)
    orig_len = len(cfg.get('inbounds', []))
    cfg['inbounds'] = [i for i in cfg.get('inbounds', []) if i.get('tag') != 'gfk-socks']
    if len(cfg['inbounds']) < orig_len:
        with open(sys.argv[1], 'w') as f:
            json.dump(cfg, f, indent=2)
except: pass
" "$XRAY_CONFIG_FILE" 2>/dev/null
        systemctl restart xray 2>/dev/null || true
    fi

    # Remove ALL paqctl firewall rules (tagged with "paqctl" comment)
    log_info "Removing firewall rules..."
    _remove_all_paqctl_firewall_rules
    # Also try the port-specific removal for backwards compatibility
    _remove_firewall

    # Remove systemd services
    if command -v systemctl &>/dev/null; then
        systemctl stop paqctl.service 2>/dev/null || true
        systemctl disable paqctl.service 2>/dev/null || true
        systemctl stop paqctl-telegram.service 2>/dev/null || true
        systemctl disable paqctl-telegram.service 2>/dev/null || true
        rm -f /etc/systemd/system/paqctl.service
        rm -f /etc/systemd/system/paqctl-telegram.service
        systemctl daemon-reload 2>/dev/null || true
    fi

    # Remove OpenRC/SysVinit
    rm -f /etc/init.d/paqctl 2>/dev/null

    # Remove symlink
    rm -f /usr/local/bin/paqctl

    # Remove install directory
    rm -rf "${INSTALL_DIR:?}"

    echo ""
    log_success "paqctl has been completely uninstalled"
    echo ""
}

#═══════════════════════════════════════════════════════════════════════
# Help
#═══════════════════════════════════════════════════════════════════════

show_help() {
    echo ""
    echo -e "${BOLD}paqctl${NC} - Paqet Manager v${VERSION}"
    echo ""
    echo -e "${BOLD}Usage:${NC} sudo paqctl <command>"
    echo ""
    echo -e "${BOLD}Commands:${NC}"
    echo "  menu        Interactive menu (default)"
    echo "  status      Show service status and configuration"
    echo ""
    echo -e "${BOLD}Backend Control (individual):${NC}"
    echo "  start-paqet   Start paqet backend only"
    echo "  stop-paqet    Stop paqet backend only"
    echo "  start-gfk     Start GFK backend only"
    echo "  stop-gfk      Stop GFK backend only"
    echo "  start-all     Start both backends"
    echo "  stop-all      Stop both backends"
    echo ""
    echo -e "${BOLD}Legacy (uses active backend):${NC}"
    echo "  start       Start active backend"
    echo "  stop        Stop active backend"
    echo "  restart     Restart active backend"
    echo ""
    echo -e "${BOLD}Other:${NC}"
    echo "  logs        View logs (live)"
    echo "  health      Run health check diagnostics"
    echo "  update      Check for and install updates"
    echo "  config      Change configuration"
    echo "  secret      Generate a new encryption key"
    echo "  firewall    Manage iptables rules"
    echo "  backup      Backup configuration"
    echo "  restore     Restore from backup"
    echo "  telegram    Telegram notification settings"
    echo "  rollback    Roll back to a previous paqet version"
    echo "  ping        Test connectivity (paqet ping)"
    echo "  dump        Capture packets for diagnostics (paqet dump)"
    echo "  uninstall   Remove paqctl completely"
    echo "  version     Show version info"
    echo "  help        Show this help"
    echo ""
    echo -e "${BOLD}Paqet:${NC} https://git.samnet.dev/SamNet-dev/paqctl"
    echo ""
}

show_version() {
    echo ""
    echo -e "  paqctl version:  ${BOLD}${VERSION}${NC}"
    if [ "$BACKEND" = "gfw-knocker" ]; then
        echo -e "  backend:         ${BOLD}gfw-knocker${NC}"
        local py_ver; py_ver=$(python3 --version 2>/dev/null || echo "unknown")
        echo -e "  python:          ${BOLD}${py_ver}${NC}"
    else
        echo -e "  paqet version:   ${BOLD}${PAQET_VERSION}${NC}"
        local bin_ver
        bin_ver=$("$INSTALL_DIR/bin/paqet" version 2>/dev/null || echo "unknown")
        echo -e "  paqet binary:    ${BOLD}${bin_ver}${NC}"
        if echo "$PAQET_VERSION" | grep -qi "alpha\|beta\|rc"; then
            echo ""
            echo -e "  ${YELLOW}Note: paqet is in alpha phase — expect breaking changes between versions.${NC}"
        fi
    fi
    echo ""
    echo -e "  ${DIM}paqctl by sam: https://git.samnet.dev/SamNet-dev/paqctl${NC}"
    echo ""
}

#═══════════════════════════════════════════════════════════════════════
# Paqet Diagnostic Tools (ping / dump)
#═══════════════════════════════════════════════════════════════════════

run_ping() {
    echo ""
    if [ "$BACKEND" = "gfw-knocker" ]; then
        log_warn "ping diagnostic is only available for paqet backend"
        return 0
    fi
    if [ ! -x "$INSTALL_DIR/bin/paqet" ]; then
        log_error "paqet binary not found"
        return 1
    fi
    if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
        log_error "config.yaml not found. Run: sudo paqctl config"
        return 1
    fi
    log_info "Running paqet ping (Ctrl+C to stop)..."
    echo ""
    "$INSTALL_DIR/bin/paqet" ping -c "$INSTALL_DIR/config.yaml" 2>&1 || true
    echo ""
}

run_dump() {
    echo ""
    if [ "$BACKEND" = "gfw-knocker" ]; then
        log_warn "dump diagnostic is only available for paqet backend"
        return 0
    fi
    if [ ! -x "$INSTALL_DIR/bin/paqet" ]; then
        log_error "paqet binary not found"
        return 1
    fi
    if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
        log_error "config.yaml not found. Run: sudo paqctl config"
        return 1
    fi
    log_info "Running paqet dump — packet capture diagnostic (Ctrl+C to stop)..."
    echo -e "${DIM}  This shows raw packets being sent and received by paqet.${NC}"
    echo ""
    "$INSTALL_DIR/bin/paqet" dump -c "$INSTALL_DIR/config.yaml" 2>&1 || true
    echo ""
}

#═══════════════════════════════════════════════════════════════════════
# Settings Menu
#═══════════════════════════════════════════════════════════════════════

show_settings_menu() {
    local redraw=true
    while true; do
        if [ "$redraw" = true ]; then
            clear
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo -e "${BOLD}  SETTINGS & TOOLS${NC}"
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo ""
            echo "  1. Change configuration"
            echo "  2. Manage firewall rules"
            echo "  3. Generate encryption key"
            echo "  4. Backup configuration"
            echo "  5. Restore from backup"
            echo "  6. Health check"
            echo "  7. Telegram notifications"
            echo "  8. Version info"
            echo "  9. Rollback to previous version"
            echo "  p. Ping test (connectivity)"
            echo "  d. Packet dump (diagnostics)"
            echo "  a. Install additional backend"
            echo "  s. Switch backend (current: ${BACKEND})"
            echo "  u. Uninstall"
            echo ""
            echo "  b. Back to main menu"
            echo ""
            redraw=false
        fi

        read -p "  Choice: " s_choice < /dev/tty || break
        case "$s_choice" in
            1) change_config; redraw=true ;;
            2) show_firewall; redraw=true ;;
            3) generate_secret; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            4) backup_config; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            5) restore_config; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            6) health_check; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            7) show_telegram_menu; redraw=true ;;
            8) show_version; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            9) rollback_paqet; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            p|P) run_ping; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            d|D) run_dump; read -n 1 -s -r -p "  Press any key..." < /dev/tty || true; redraw=true ;;
            a|A) install_additional_backend; redraw=true ;;
            s|S) switch_backend; redraw=true ;;
            u|U) uninstall_paqctl; exit 0 ;;
            b|B) return ;;
            "") ;;
            *) echo -e "  ${RED}Invalid choice${NC}" ;;
        esac
    done
}

#═══════════════════════════════════════════════════════════════════════
# Info Menu
#═══════════════════════════════════════════════════════════════════════

show_info_menu() {
    local redraw=true
    while true; do
        if [ "$redraw" = true ]; then
            clear
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo -e "${BOLD}  INFO & HELP${NC}"
            echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
            echo ""
            echo "  1. How Paqet Works"
            echo "  2. Server vs Client Mode"
            echo "  3. Firewall Rules Explained"
            echo "  4. Troubleshooting"
            echo "  5. About"
            echo ""
            echo "  b. Back"
            echo ""
            redraw=false
        fi

        read -p "  Choice: " i_choice < /dev/tty || break
        case "$i_choice" in
            1)
                clear
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo -e "${BOLD}  HOW PAQET WORKS${NC}"
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo ""
                echo -e "  ${BOLD}Overview:${NC}"
                echo "  Paqet is a bidirectional packet-level proxy written in Go."
                echo "  Unlike traditional proxies (Shadowsocks, V2Ray, etc.) that"
                echo "  operate at the application or transport layer, paqet works"
                echo "  at the raw socket level — below the OS network stack."
                echo ""
                echo -e "  ${BOLD}How it works step by step:${NC}"
                echo ""
                echo "  1. PACKET CRAFTING"
                echo "     Paqet uses gopacket + libpcap to craft TCP packets"
                echo "     directly, bypassing the kernel's TCP/IP stack entirely."
                echo "     This means the OS doesn't even know there's a connection."
                echo ""
                echo "  2. KCP ENCRYPTED TRANSPORT"
                echo "     All traffic between client and server is encrypted using"
                echo "     the KCP protocol with AES symmetric key encryption."
                echo "     KCP provides reliable, ordered delivery over raw packets"
                echo "     with built-in error correction and retransmission."
                echo ""
                echo "  3. CONNECTION MULTIPLEXING"
                echo "     Multiple connections are multiplexed over a single KCP"
                echo "     session using smux, reducing overhead and improving"
                echo "     performance for concurrent requests."
                echo ""
                echo "  4. FIREWALL BYPASS"
                echo "     Because it operates below the OS network stack, paqet"
                echo "     bypasses traditional firewalls (ufw, firewalld) and"
                echo "     kernel-level connection tracking (conntrack). The OS"
                echo "     firewall never sees the traffic as a 'connection'."
                echo ""
                echo "  5. SOCKS5 PROXY (Client)"
                echo "     On the client side, paqet exposes a standard SOCKS5"
                echo "     proxy that any application can use. Traffic enters"
                echo "     the SOCKS5 port, gets encrypted and sent via raw"
                echo "     packets to the server, which forwards it to the"
                echo "     destination on the open internet."
                echo ""
                echo -e "  ${DIM}Technical stack: Go, gopacket, libpcap, KCP, smux, AES${NC}"
                echo -e "  ${DIM}Project: https://git.samnet.dev/SamNet-dev/paqctl${NC}"
                echo ""
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            2)
                clear
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo -e "${BOLD}  SERVER VS CLIENT MODE${NC}"
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo ""
                echo -e "  ${GREEN}${BOLD}SERVER MODE${NC}"
                echo -e "  ${DIM}─────────────────────────────────────────────${NC}"
                echo "  The server is the exit node. It receives encrypted raw"
                echo "  packets from clients, decrypts them, and forwards the"
                echo "  traffic to the open internet. Responses are encrypted"
                echo "  and sent back to the client."
                echo ""
                echo "  Requirements:"
                echo "  - A server with a public IP address"
                echo "  - Root access (raw sockets need it)"
                echo "  - libpcap installed"
                echo "  - iptables NOTRACK + RST DROP rules (auto-managed)"
                echo "  - An open port (paqctl manages firewall rules, but you"
                echo "    may need to allow the port in your cloud provider's"
                echo "    security group / network firewall)"
                echo ""
                echo "  After setup, share with your clients:"
                echo "    - Server IP and port (e.g. 1.2.3.4:8443)"
                echo "    - Encryption key (generated during setup)"
                echo ""
                echo -e "  ${CYAN}${BOLD}CLIENT MODE${NC}"
                echo -e "  ${DIM}─────────────────────────────────────────────${NC}"
                echo "  The client connects to a paqet server and provides a"
                echo "  local SOCKS5 proxy. Applications on your machine connect"
                echo "  to the SOCKS5 port, and traffic is tunneled through"
                echo "  paqet's encrypted raw-socket connection to the server."
                echo ""
                echo "  Requirements:"
                echo "  - Server IP:PORT and encryption key from the server admin"
                echo "  - Root access (raw sockets need it)"
                echo "  - libpcap installed"
                echo ""
                echo "  Usage after setup:"
                echo "    Browser:  Set SOCKS5 proxy to 127.0.0.1:1080"
                echo "    curl:     curl --proxy socks5h://127.0.0.1:1080 URL"
                echo "    System:   Configure system proxy to SOCKS5 127.0.0.1:1080"
                echo ""
                echo -e "  ${BOLD}Data flow:${NC}"
                echo "    App -> SOCKS5(:1080) -> paqet client -> raw packets"
                echo "    -> internet -> paqet server -> destination website"
                echo ""
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            3)
                clear
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo -e "${BOLD}  FIREWALL RULES EXPLAINED${NC}"
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo ""
                echo "  Paqet requires specific iptables rules on the SERVER."
                echo "  These rules are needed because paqet crafts raw TCP"
                echo "  packets, and without them the kernel interferes."
                echo ""
                echo -e "  ${BOLD}Rule 1: PREROUTING NOTRACK${NC}"
                echo "  iptables -t raw -A PREROUTING -p tcp --dport PORT -j NOTRACK"
                echo ""
                echo "  WHY: Tells the kernel's connection tracker (conntrack) to"
                echo "  ignore incoming packets on the paqet port. Without this,"
                echo "  conntrack tries to match packets to connections it doesn't"
                echo "  know about and may drop them."
                echo ""
                echo -e "  ${BOLD}Rule 2: OUTPUT NOTRACK${NC}"
                echo "  iptables -t raw -A OUTPUT -p tcp --sport PORT -j NOTRACK"
                echo ""
                echo "  WHY: Same as above but for outgoing packets. Prevents"
                echo "  conntrack from tracking paqet's outbound raw packets."
                echo ""
                echo -e "  ${BOLD}Rule 3: RST DROP${NC}"
                echo "  iptables -t mangle -A OUTPUT -p tcp --sport PORT"
                echo "           --tcp-flags RST RST -j DROP"
                echo ""
                echo "  WHY: When the kernel sees incoming TCP SYN packets on a"
                echo "  port with no listening socket, it sends TCP RST (reset)"
                echo "  back. This would kill paqet connections. This rule drops"
                echo "  those RST packets so paqet can handle them instead."
                echo ""
                echo -e "  ${DIM}These rules are auto-managed by paqctl:${NC}"
                echo -e "  ${DIM}  - Applied on service start (ExecStartPre)${NC}"
                echo -e "  ${DIM}  - Removed on service stop (ExecStopPost)${NC}"
                echo -e "  ${DIM}  - Persisted across reboots when possible${NC}"
                echo ""
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            4)
                clear
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo -e "${BOLD}  TROUBLESHOOTING${NC}"
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo ""
                echo -e "  ${BOLD}Service won't start:${NC}"
                echo "  1. Check logs:        sudo paqctl logs"
                echo "  2. Run health check:  sudo paqctl health"
                echo "  3. Verify libpcap:    ldconfig -p | grep libpcap"
                echo "  4. Check config:      cat /opt/paqctl/config.yaml"
                echo "  5. Test binary:       sudo /opt/paqctl/bin/paqet version"
                echo ""
                echo -e "  ${BOLD}Client can't connect to server:${NC}"
                echo "  1. Verify server IP and port are correct"
                echo "  2. Check encryption key matches exactly"
                echo "  3. Ensure server iptables rules are active:"
                echo "       sudo paqctl firewall  (on server)"
                echo "  4. Check cloud security group allows the port"
                echo "  5. Test raw connectivity:"
                echo "       sudo /opt/paqctl/bin/paqet ping -c /opt/paqctl/config.yaml"
                echo "  6. Run packet dump to see what's happening:"
                echo "       sudo /opt/paqctl/bin/paqet dump -c /opt/paqctl/config.yaml"
                echo ""
                echo -e "  ${BOLD}SOCKS5 not working (client side):${NC}"
                echo "  1. Verify client is running:  sudo paqctl status"
                echo "  2. Test the proxy directly:"
                echo "       curl -v --proxy socks5h://127.0.0.1:1080 https://httpbin.org/ip"
                echo "  3. Check SOCKS port is listening:"
                echo "       ss -tlnp | grep 1080"
                echo "  4. Check if paqet output shows errors:"
                echo "       sudo paqctl logs"
                echo ""
                echo -e "  ${BOLD}High CPU / Memory:${NC}"
                echo "  1. Check process stats:  sudo paqctl status"
                echo "  2. Restart the service:  sudo paqctl restart"
                echo "  3. Check for latest version: sudo paqctl update"
                echo ""
                echo -e "  ${BOLD}After system reboot:${NC}"
                echo "  1. paqctl auto-starts via systemd (check: systemctl status paqctl)"
                echo "  2. iptables rules are re-applied by ExecStartPre"
                echo "  3. If rules are missing: sudo paqctl firewall -> Apply"
                echo ""
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            5)
                clear
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo -e "${BOLD}  ABOUT${NC}"
                echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
                echo ""
                echo -e "  ${BOLD}paqctl v${VERSION}${NC} - Paqet Management Tool"
                echo ""
                echo -e "  ${CYAN}── Paqet ──${NC}"
                echo ""
                echo -e "  ${BOLD}Creator:${NC}    hanselime"
                echo -e "  ${BOLD}Repository:${NC} https://git.samnet.dev/SamNet-dev/paqctl"
                echo -e "  ${BOLD}License:${NC}    AGPL-3.0 - Copyright (C) 2026 SamNet Technologies, LLC"
                echo -e "  ${BOLD}Language:${NC}   Go"
                echo -e "  ${BOLD}Contact:${NC}    Signal @hanselime.11"
                echo ""
                echo "  Paqet is a bidirectional packet-level proxy that uses"
                echo "  KCP over raw TCP packets with custom TCP flags."
                echo "  It operates below the OS TCP/IP stack to bypass"
                echo "  firewalls and deep packet inspection."
                echo ""
                echo "  Features:"
                echo "  - Raw TCP packet crafting via gopacket"
                echo "  - KCP + AES symmetric encryption"
                echo "  - SOCKS5 proxy for dynamic connections"
                echo "  - Connection multiplexing via smux"
                echo "  - Cross-platform (Linux, macOS, Windows)"
                echo "  - Android client: github.com/AliRezaBeigy/paqetNG"
                echo ""
                echo -e "  ${CYAN}── paqctl Management Tool ──${NC}"
                echo ""
                echo -e "  ${BOLD}Built by:${NC}   SamNet-dev"
                echo -e "  ${BOLD}Repository:${NC} https://git.samnet.dev/SamNet-dev/paqctl"
                echo -e "  ${BOLD}License:${NC}    AGPL-3.0 - Copyright (C) 2026 SamNet Technologies, LLC"
                echo ""
                echo "  paqctl provides one-click installation, configuration,"
                echo "  service management, auto-updates, health monitoring,"
                echo "  and Telegram notifications for paqet."
                echo ""
                echo -e "  ${DIM}Original paqet by hanselime, improved by SamNet.${NC}"
                echo ""
                read -n 1 -s -r -p "  Press any key..." < /dev/tty || true
                redraw=true
                ;;
            b|B) return ;;
            "") ;;
            *) echo -e "  ${RED}Invalid choice${NC}" ;;
        esac
    done
}

#═══════════════════════════════════════════════════════════════════════
# Connection Info Display
#═══════════════════════════════════════════════════════════════════════

show_connection_info() {
    clear
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD}  CLIENT CONNECTION INFO${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""

    _load_settings

    local local_ip
    local_ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown")

    local paqet_installed=false
    local gfk_installed=false
    [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
    if [ "$ROLE" = "server" ]; then
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
    else
        [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
    fi

    if [ "$paqet_installed" = true ]; then
        echo -e "  ${GREEN}${BOLD}━━━ PAQET ━━━${NC}"
        echo ""
        local paqet_port="${LISTEN_PORT:-8443}"
        local paqet_key="${ENCRYPTION_KEY:-not set}"
        # Try to get key from config if not in settings
        if [ "$paqet_key" = "not set" ] && [ -f "$INSTALL_DIR/config.yaml" ]; then
            paqet_key=$(grep -E "^key:" "$INSTALL_DIR/config.yaml" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "not set")
        fi
        echo -e "  ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}"
        echo -e "  ${YELLOW}║${NC}  Server:  ${BOLD}${local_ip}:${paqet_port}${NC}"
        echo -e "  ${YELLOW}║${NC}  Key:     ${BOLD}${paqet_key}${NC}"
        echo -e "  ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}"
        echo ""
        echo -e "  ${DIM}Client proxy: 127.0.0.1:1080 (SOCKS5)${NC}"
        echo ""
    fi

    if [ "$gfk_installed" = true ]; then
        echo -e "  ${MAGENTA}${BOLD}━━━ GFW-KNOCKER ━━━${NC}"
        echo ""
        local gfk_ip="${GFK_SERVER_IP:-$local_ip}"
        local gfk_auth="${GFK_AUTH_CODE:-not set}"
        local gfk_mappings="${GFK_PORT_MAPPINGS:-14000:443}"
        echo -e "  ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}"
        echo -e "  ${YELLOW}║${NC}  Server IP:  ${BOLD}${gfk_ip}${NC}"
        echo -e "  ${YELLOW}║${NC}  Auth Code:  ${BOLD}${gfk_auth}${NC}"
        echo -e "  ${YELLOW}║${NC}  Mappings:   ${BOLD}${gfk_mappings}${NC}"
        echo -e "  ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}"
        echo ""
        echo -e "  ${DIM}VIO port: ${GFK_VIO_PORT:-45000} | QUIC port: ${GFK_QUIC_PORT:-25000}${NC}"
        local _gfk_proxy_port
        if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
            _gfk_proxy_port="$GFK_SOCKS_VIO_PORT"
        else
            _gfk_proxy_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
        fi
        echo -e "  ${DIM}Client proxy: 127.0.0.1:${_gfk_proxy_port} (SOCKS5)${NC}"
        echo ""
    fi

    if [ "$paqet_installed" = false ] && [ "$gfk_installed" = false ]; then
        echo -e "  ${YELLOW}No backends installed yet.${NC}"
        echo ""
        echo "  Run 'sudo paqctl menu' and select 'Settings & Tools'"
        echo "  to install a backend."
        echo ""
    fi

    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""
    read -n 1 -s -r -p "  Press any key to return..." < /dev/tty || true
}

#═══════════════════════════════════════════════════════════════════════
# Interactive Menu
#═══════════════════════════════════════════════════════════════════════

show_menu() {
    # Auto-fix systemd service if in failed state
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        local svc_state=$(systemctl is-active paqctl.service 2>/dev/null)
        if [ "$svc_state" = "failed" ]; then
            systemctl reset-failed paqctl.service 2>/dev/null || true
        fi
    fi

    # Reload settings
    _load_settings

    local paqet_installed=false
    local gfk_installed=false
    local redraw=true

    while true; do
        if [ "$redraw" = true ]; then
            # Re-check what's installed each redraw
            paqet_installed=false
            gfk_installed=false
            [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
            if [ "$ROLE" = "server" ]; then
                [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
            else
                [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
            fi

            clear
            print_header

            # Status line showing both backends
            echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
            echo -e "  ${BOLD}BACKEND STATUS${NC}  (Role: ${ROLE})"
            echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"

            # Paqet status
            if [ "$paqet_installed" = true ]; then
                local _paqet_info=""
                if [ "$ROLE" = "server" ]; then
                    _paqet_info="Port: ${LISTEN_PORT:-8443}"
                else
                    _paqet_info="Server: ${REMOTE_SERVER:-N/A}"
                fi
                if is_paqet_running; then
                    echo -e "  Paqet:       ${GREEN}● Running${NC}  |  ${_paqet_info}  |  SOCKS5: 127.0.0.1:${SOCKS_PORT:-1080}"
                else
                    echo -e "  Paqet:       ${RED}○ Stopped${NC}  |  ${_paqet_info}"
                fi
            else
                echo -e "  Paqet:       ${DIM}not installed${NC}"
            fi

            # GFK status
            if [ "$gfk_installed" = true ]; then
                if is_gfk_running; then
                    local _gfk_sv
                    if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                        _gfk_sv="$GFK_SOCKS_VIO_PORT"
                    else
                        _gfk_sv=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
                    fi
                    echo -e "  GFK:         ${GREEN}● Running${NC}  |  VIO: ${GFK_VIO_PORT:-45000}  |  SOCKS5: 127.0.0.1:${_gfk_sv}"
                else
                    echo -e "  GFK:         ${RED}○ Stopped${NC}  |  VIO: ${GFK_VIO_PORT:-45000}"
                fi
            else
                echo -e "  GFK:         ${DIM}not installed${NC}"
            fi

            echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"

            echo ""
            echo -e "  ${CYAN}MAIN MENU${NC}"
            echo ""
            echo "  1. View status"
            echo "  2. View logs"
            echo "  3. Health check"
            echo "  4. Update"
            echo ""

            # Paqet controls
            if [ "$paqet_installed" = true ]; then
                if is_paqet_running; then
                    echo -e "  p. ${RED}Stop${NC} Paqet"
                else
                    echo -e "  p. ${GREEN}Start${NC} Paqet"
                fi
            fi

            # GFK controls
            if [ "$gfk_installed" = true ]; then
                if is_gfk_running; then
                    echo -e "  g. ${RED}Stop${NC} GFK"
                else
                    echo -e "  g. ${GREEN}Start${NC} GFK"
                fi
            fi

            # Start/Stop all
            if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
                echo ""
                if is_paqet_running && is_gfk_running; then
                    echo -e "  a. ${RED}Stop ALL${NC} backends"
                elif ! is_paqet_running && ! is_gfk_running; then
                    echo -e "  a. ${GREEN}Start ALL${NC} backends"
                else
                    echo "  a. Toggle ALL backends"
                fi
            fi

            echo ""
            echo "  8. Settings & Tools"
            echo -e "  ${YELLOW}c. Connection Info${NC}"
            echo "  i. Info & Help"
            echo -e "  ${RED}u. Uninstall${NC}"
            echo "  0. Exit"
            echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
            echo ""
            redraw=false
        fi

        echo -n "  Select option: "
        if ! read choice < /dev/tty 2>/dev/null; then
            log_error "Cannot read input. If piped, run: sudo paqctl menu"
            exit 1
        fi

        case "$choice" in
            1) show_status; read -n 1 -s -r -p "  Press any key to return..." < /dev/tty || true; redraw=true ;;
            2) show_logs; redraw=true ;;
            3) health_check; read -n 1 -s -r -p "  Press any key to return..." < /dev/tty || true; redraw=true ;;
            4) update_paqet; read -n 1 -s -r -p "  Press any key to return..." < /dev/tty || true; redraw=true ;;
            p|P)
                if [ "$paqet_installed" = true ]; then
                    if is_paqet_running; then
                        stop_paqet_backend
                    else
                        start_paqet_backend
                    fi
                    read -n 1 -s -r -p "  Press any key to return..." < /dev/tty || true
                else
                    echo -e "  ${YELLOW}Paqet not installed${NC}"
                fi
                redraw=true
                ;;
            g|G)
                if [ "$gfk_installed" = true ]; then
                    if is_gfk_running; then
                        stop_gfk_backend
                    else
                        start_gfk_backend
                    fi
                    read -n 1 -s -r -p "  Press any key to return..." < /dev/tty || true
                else
                    echo -e "  ${YELLOW}GFK not installed${NC}"
                fi
                redraw=true
                ;;
            a|A)
                if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
                    if is_paqet_running && is_gfk_running; then
                        # Stop all
                        stop_paqet_backend
                        stop_gfk_backend
                    elif ! is_paqet_running && ! is_gfk_running; then
                        # Start all
                        start_paqet_backend
                        start_gfk_backend
                    else
                        # Mixed state - ask user
                        echo ""
                        echo "  1. Start all backends"
                        echo "  2. Stop all backends"
                        echo -n "  Choice: "
                        read subchoice < /dev/tty || true
                        case "$subchoice" in
                            1)
                                [ "$paqet_installed" = true ] && ! is_paqet_running && start_paqet_backend
                                [ "$gfk_installed" = true ] && ! is_gfk_running && start_gfk_backend
                                ;;
                            2)
                                is_paqet_running && stop_paqet_backend
                                is_gfk_running && stop_gfk_backend
                                ;;
                        esac
                    fi
                    read -n 1 -s -r -p "  Press any key to return..." < /dev/tty || true
                fi
                redraw=true
                ;;
            8) show_settings_menu; redraw=true ;;
            c|C) show_connection_info; redraw=true ;;
            i|I) show_info_menu; redraw=true ;;
            u|U) uninstall_paqctl; exit 0 ;;
            0) echo "  Exiting."; exit 0 ;;
            "") ;;
            *) echo -e "  ${RED}Invalid choice: ${NC}${YELLOW}$choice${NC}" ;;
        esac
    done
}

#═══════════════════════════════════════════════════════════════════════
# CLI Command Router
#═══════════════════════════════════════════════════════════════════════

case "${1:-menu}" in
    status)           show_status ;;
    start)            start_paqet ;;
    stop)             stop_paqet ;;
    restart)          restart_paqet ;;
    start-paqet)      start_paqet_backend ;;
    stop-paqet)       stop_paqet_backend ;;
    start-gfk)        start_gfk_backend ;;
    stop-gfk)         stop_gfk_backend ;;
    start-all)        start_paqet_backend; start_gfk_backend ;;
    stop-all)         stop_paqet_backend; stop_gfk_backend ;;
    logs)             show_logs ;;
    health)           health_check ;;
    update)           update_paqet ;;
    config)           change_config ;;
    secret)           generate_secret ;;
    firewall)         show_firewall ;;
    rollback)         rollback_paqet ;;
    ping)             run_ping ;;
    dump)             run_dump ;;
    backup)           backup_config ;;
    restore)          restore_config ;;
    telegram)         show_telegram_menu ;;
    uninstall)        uninstall_paqctl ;;
    version)          show_version ;;
    help|--help|-h)   show_help ;;
    menu)             show_menu ;;
    _apply-firewall)  _apply_firewall ;;
    _remove-firewall) _remove_firewall ;;
    *)
        echo -e "${RED}Unknown command: $1${NC}"
        echo "Run 'sudo paqctl help' for usage."
        exit 1
        ;;
esac
MANAGEMENT

    # Replace placeholder
    sed "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$tmp_script" > "$tmp_script.sed" && mv "$tmp_script.sed" "$tmp_script"

    if ! chmod +x "$tmp_script"; then
        log_error "Failed to make management script executable"
        rm -f "$tmp_script"
        return 1
    fi
    if ! mv -f "$tmp_script" "$INSTALL_DIR/paqctl"; then
        log_error "Failed to install management script"
        rm -f "$tmp_script"
        return 1
    fi

    # Create symlink
    rm -f /usr/local/bin/paqctl 2>/dev/null
    if ! ln -sf "$INSTALL_DIR/paqctl" /usr/local/bin/paqctl; then
        log_warn "Failed to create symlink /usr/local/bin/paqctl"
    fi

    log_success "Management script installed → /usr/local/bin/paqctl"
}

#═══════════════════════════════════════════════════════════════════════
# Main Installation Flow
#═══════════════════════════════════════════════════════════════════════

_load_settings() {
    [ -f "$INSTALL_DIR/settings.conf" ] || return 0
    # Safe settings loading without eval
    while IFS='=' read -r key value; do
        [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
        value="${value#\"}"; value="${value%\"}"
        # Skip values with dangerous shell characters
        [[ "$value" =~ [\`\$\(] ]] && continue
        case "$key" in
            BACKEND) BACKEND="$value" ;;
            ROLE) ROLE="$value" ;;
            PAQET_VERSION) PAQET_VERSION="$value" ;;
            PAQCTL_VERSION) PAQCTL_VERSION="$value" ;;
            LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;;
            SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;;
            INTERFACE) INTERFACE="$value" ;;
            LOCAL_IP) LOCAL_IP="$value" ;;
            GATEWAY_MAC) GATEWAY_MAC="$value" ;;
            ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;;
            PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;;
            PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$value" ;;
            REMOTE_SERVER) REMOTE_SERVER="$value" ;;
            GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;;
            GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;;
            GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;;
            GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;;
            GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;;
            GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;;
            GFK_SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_PORT="$value" ;;
            GFK_SOCKS_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_VIO_PORT="$value" ;;
            XRAY_PANEL_DETECTED) XRAY_PANEL_DETECTED="$value" ;;
            MICROSOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && MICROSOCKS_PORT="$value" ;;
            GFK_SERVER_IP) GFK_SERVER_IP="$value" ;;
            GFK_TCP_FLAGS) [[ "$value" =~ ^[FSRPAUEC]+$ ]] && GFK_TCP_FLAGS="$value" ;;
            TELEGRAM_BOT_TOKEN) TELEGRAM_BOT_TOKEN="$value" ;;
            TELEGRAM_CHAT_ID) TELEGRAM_CHAT_ID="$value" ;;
            TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_INTERVAL="$value" ;;
            TELEGRAM_ENABLED) TELEGRAM_ENABLED="$value" ;;
            TELEGRAM_ALERTS_ENABLED) TELEGRAM_ALERTS_ENABLED="$value" ;;
            TELEGRAM_DAILY_SUMMARY) TELEGRAM_DAILY_SUMMARY="$value" ;;
            TELEGRAM_WEEKLY_SUMMARY) TELEGRAM_WEEKLY_SUMMARY="$value" ;;
            TELEGRAM_SERVER_LABEL) TELEGRAM_SERVER_LABEL="$value" ;;
            TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_START_HOUR="$value" ;;
        esac
    done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
}

# Handle --update-components flag (called during self-update)
if [ "${1:-}" = "--update-components" ]; then
    INSTALL_DIR="${INSTALL_DIR:-/opt/paqctl}"
    _load_settings
    create_management_script
    exit 0
fi

main() {
    check_root
    print_header

    # Check if already installed
    if [ -f "$INSTALL_DIR/settings.conf" ] && { [ -x "$INSTALL_DIR/bin/paqet" ] || [ -f "$GFK_DIR/mainserver.py" ]; }; then
        _load_settings
        log_info "paqctl is already installed (backend: ${BACKEND:-paqet})."
        echo ""
        echo "  1. Reinstall / Reconfigure"
        echo "  2. Open menu  (same as: sudo paqctl menu)"
        echo "  3. Exit"
        echo ""
        read -p "  Choice [1-3]: " choice < /dev/tty || true
        case "$choice" in
            1) log_info "Reinstalling..." ;;
            2) exec /usr/local/bin/paqctl menu ;;
            *) exit 0 ;;
        esac
    fi

    # Step 1: Detect OS
    log_info "Step 1/7: Detecting operating system..."
    detect_os
    echo ""

    # Step 2: Install dependencies
    log_info "Step 2/7: Installing dependencies..."
    check_dependencies
    echo ""

    # Step 3: Configuration wizard (determines backend + role + config)
    log_info "Step 3/7: Configuration..."
    run_config_wizard
    echo ""

    # Step 4: Backend-specific dependencies and download
    log_info "Step 4/7: Setting up ${BACKEND} backend..."
    if [ "$BACKEND" = "gfw-knocker" ]; then
        install_python_deps || { log_error "Failed to install Python dependencies"; exit 1; }
        download_gfk || { log_error "Failed to download GFK"; exit 1; }
        generate_gfk_certs || { log_error "Failed to generate certificates"; exit 1; }
        if [ "$ROLE" = "server" ]; then
            # Install Xray SOCKS5 proxy (adds alongside panel if detected)
            setup_xray_for_gfk || { log_error "Failed to setup Xray"; exit 1; }
            # Regenerate config if mappings changed (panel detected → SOCKS5 added)
            if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
                generate_gfk_config || { log_error "Failed to regenerate GFK config"; exit 1; }
            fi
        elif [ "$ROLE" = "client" ]; then
            create_gfk_client_wrapper || { log_error "Failed to create client wrapper"; exit 1; }
        fi
        PAQET_VERSION="$GFK_VERSION_PINNED"
        log_info "Using GFK ${PAQET_VERSION} (pinned for stability)"
    else
        # Fetch latest version from GitHub, fall back to pinned if API unreachable
        PAQET_VERSION=$(curl -s --max-time 10 "$PAQET_API_URL" 2>/dev/null | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
        if [ -z "$PAQET_VERSION" ] || ! _validate_version_tag "$PAQET_VERSION"; then
            PAQET_VERSION="$PAQET_VERSION_PINNED"
        fi
        log_info "Installing paqet ${PAQET_VERSION}"
        download_paqet "$PAQET_VERSION"
    fi
    echo ""

    # Step 5: Apply firewall rules
    log_info "Step 5/7: Firewall setup..."
    if [ "$BACKEND" = "gfw-knocker" ]; then
        if [ "$ROLE" = "server" ]; then
            local _vio_port="${GFK_VIO_PORT:-45000}"
            log_info "Blocking VIO TCP port $_vio_port (raw socket handles it)..."
            if _is_firewalld_active; then
                firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO INPUT DROP rule via firewalld"
                firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO RST DROP rule via firewalld"
                firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                persist_iptables_rules
            elif command -v iptables &>/dev/null; then
                modprobe iptable_raw 2>/dev/null || true
                iptables -t raw -C PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    iptables -t raw -A PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                iptables -t raw -C OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    iptables -t raw -A OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    iptables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO INPUT DROP rule"
                iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    iptables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO RST DROP rule"
                if command -v ip6tables &>/dev/null; then
                    ip6tables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                        ip6tables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                    ip6tables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                        ip6tables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                fi
            else
                log_warn "iptables not found - firewall rules cannot be applied"
            fi
        else
            local _vio_client_port="${GFK_VIO_CLIENT_PORT:-40000}"
            log_info "Applying NOTRACK + DROP rules for VIO client port $_vio_client_port..."
            if _is_firewalld_active; then
                firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO client INPUT DROP rule via firewalld"
                firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO client RST DROP rule via firewalld"
                firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                persist_iptables_rules
            elif command -v iptables &>/dev/null; then
                modprobe iptable_raw 2>/dev/null || true
                iptables -t raw -C PREROUTING -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    iptables -t raw -A PREROUTING -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                iptables -t raw -C OUTPUT -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
                    iptables -t raw -A OUTPUT -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
                iptables -C INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    iptables -A INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO client INPUT DROP rule"
                iptables -C OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    iptables -A OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                    log_warn "Failed to add VIO client RST DROP rule"
                if command -v ip6tables &>/dev/null; then
                    ip6tables -C INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                        ip6tables -A INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                    ip6tables -C OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
                        ip6tables -A OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
                fi
            else
                log_warn "iptables not found - firewall rules cannot be applied"
            fi
        fi
    elif [ "$ROLE" = "server" ]; then
        apply_iptables_rules "$LISTEN_PORT"
    else
        log_info "Client mode - no firewall rules needed"
    fi
    echo ""

    # Step 6: Create service + management script
    log_info "Step 6/7: Setting up service..."
    if ! mkdir -p "$INSTALL_DIR/bin" "$BACKUP_DIR"; then
        log_error "Failed to create installation directories"
        exit 1
    fi
    create_management_script
    setup_service
    setup_logrotate
    # Save settings to persist version and config
    save_settings
    echo ""

    # Step 7: Start the service
    log_info "Step 7/7: Starting ${BACKEND}..."
    if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
        systemctl start paqctl.service 2>/dev/null
    fi

    sleep 2

    # Final summary
    echo ""
    echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${GREEN}${BOLD}  INSTALLATION COMPLETE!${NC}"
    echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""
    echo -e "  Backend:    ${BOLD}${BACKEND}${NC}"
    echo -e "  Role:       ${BOLD}${ROLE}${NC}"
    echo -e "  Version:    ${BOLD}${PAQET_VERSION}${NC}"

    if [ "$BACKEND" = "gfw-knocker" ]; then
        if [ "$ROLE" = "server" ]; then
            local _xray_port
            _xray_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
            echo -e "  VIO port:   ${BOLD}${GFK_VIO_PORT}${NC}"
            echo -e "  QUIC port:  ${BOLD}${GFK_QUIC_PORT}${NC}"
            if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
                echo -e "  Xray:       ${BOLD}Existing panel detected (forwarding to port ${_xray_port})${NC}"
                if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                    echo -e "  SOCKS5:     ${BOLD}127.0.0.1:${GFK_SOCKS_PORT} (auto-added, VIO port ${GFK_SOCKS_VIO_PORT})${NC}"
                    echo ""
                    echo -e "  ${GREEN}✓ GFK forwards to panel + SOCKS5 proxy added${NC}"
                else
                    echo ""
                    echo -e "  ${GREEN}✓ GFK forwards to panel${NC}"
                fi
                local _first_vio
                _first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
                echo -e "  ${YELLOW}! Panel users: configure Iran outbound → 127.0.0.1:${_first_vio}${NC}"
                if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                    echo -e "  ${YELLOW}! Direct SOCKS5: use 127.0.0.1:${GFK_SOCKS_VIO_PORT} on client${NC}"
                fi
            else
                echo -e "  Xray:       ${BOLD}127.0.0.1:${_xray_port} (SOCKS5)${NC}"
                echo ""
                echo -e "  ${GREEN}✓ Xray SOCKS5 proxy installed and running${NC}"
            fi
            echo ""
            echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════╗${NC}"
            echo -e "${YELLOW}║  ${BOLD}CLIENT CONNECTION INFO - SAVE THIS!${NC}${YELLOW}                          ║${NC}"
            echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════╣${NC}"
            echo -e "${YELLOW}║${NC}  Server IP:  ${BOLD}${GFK_SERVER_IP}${NC}"
            echo -e "${YELLOW}║${NC}  Auth Code:  ${BOLD}${GFK_AUTH_CODE}${NC}"
            echo -e "${YELLOW}║${NC}  Mappings:   ${BOLD}${GFK_PORT_MAPPINGS}${NC}"
            if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ] && [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                echo -e "${YELLOW}║${NC}"
                echo -e "${YELLOW}║${NC}  ${GREEN}Proxy port:  127.0.0.1:${GFK_SOCKS_VIO_PORT} (SOCKS5 — use this on client)${NC}"
                local _panel_vio
                _panel_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
                echo -e "${YELLOW}║${NC}  Panel port: 127.0.0.1:${_panel_vio} (vmess/vless — for panel-to-panel)"
            elif [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
                local _panel_vio
                _panel_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
                echo -e "${YELLOW}║${NC}"
                echo -e "${YELLOW}║${NC}  Panel port: 127.0.0.1:${_panel_vio} (vmess/vless — for panel-to-panel)"
            else
                local _proxy_vio
                _proxy_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
                echo -e "${YELLOW}║${NC}"
                echo -e "${YELLOW}║${NC}  ${GREEN}Proxy port:  127.0.0.1:${_proxy_vio} (SOCKS5 — use this on client)${NC}"
            fi
            echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}"
        else
            local _socks_vio
            if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
                _socks_vio="$GFK_SOCKS_VIO_PORT"
            else
                _socks_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
            fi
            echo -e "  Server:     ${BOLD}${GFK_SERVER_IP}${NC}"
            echo -e "  SOCKS5:     ${BOLD}127.0.0.1:${_socks_vio}${NC}"
            echo ""
            echo -e "  ${YELLOW}Test your proxy:${NC}"
            echo -e "  ${BOLD}  curl --proxy socks5h://127.0.0.1:${_socks_vio} https://httpbin.org/ip${NC}"
        fi
    elif [ "$ROLE" = "server" ]; then
        echo -e "  Port:       ${BOLD}${LISTEN_PORT}${NC}"
        echo ""
        echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════╗${NC}"
        echo -e "${YELLOW}║  ${BOLD}CLIENT CONNECTION INFO - SAVE THIS!${NC}${YELLOW}                          ║${NC}"
        echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════╣${NC}"
        echo -e "${YELLOW}║${NC}  Server:  ${BOLD}${LOCAL_IP}:${LISTEN_PORT}${NC}"
        echo -e "${YELLOW}║${NC}  Key:     ${BOLD}${ENCRYPTION_KEY}${NC}"
        echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}"
        echo ""
        echo -e "  ${CYAN}Key also saved in: ${INSTALL_DIR}/config.yaml${NC}"
    else
        echo -e "  Server:     ${BOLD}${REMOTE_SERVER}${NC}"
        echo -e "  SOCKS5:     ${BOLD}127.0.0.1:${SOCKS_PORT}${NC}"
        echo ""
        echo -e "  ${YELLOW}Test your proxy:${NC}"
        echo -e "  ${BOLD}  curl --proxy socks5h://127.0.0.1:${SOCKS_PORT} https://httpbin.org/ip${NC}"
    fi

    echo ""
    echo -e "  ${CYAN}Management commands:${NC}"
    echo "    sudo paqctl menu       Interactive menu"
    echo "    sudo paqctl status     Check status"
    echo "    sudo paqctl health     Health check"
    echo "    sudo paqctl logs       View logs"
    echo "    sudo paqctl update     Update paqet"
    echo "    sudo paqctl help       All commands"
    echo ""
    echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""
    echo -e "  ${BOLD}${YELLOW}⚠ IMPORTANT: Save the connection info above before continuing!${NC}"
    echo ""
    echo -e "  ${CYAN}Press Y to open the management menu, or any other key to exit...${NC}"
    read -n 1 -r choice < /dev/tty || true
    echo ""
    if [[ "$choice" =~ ^[Yy]$ ]]; then
        exec /usr/local/bin/paqctl menu
    else
        echo -e "  ${GREEN}Run 'sudo paqctl menu' when ready.${NC}"
        echo ""
    fi
}

# Handle command line arguments
case "${1:-}" in
    menu)
        check_root
        if [ -f "$INSTALL_DIR/settings.conf" ]; then
            _load_settings
            show_menu
        else
            echo -e "${RED}paqctl is not installed. Run the installer first.${NC}"
            exit 1
        fi
        ;;
    *)
        main "$@"
        ;;
esac
