Ctrl K
ComunidadGuías…

install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/bash
#
# CrowdSec UniFi Suite - Interactive Installer
# One-command installer for the complete CrowdSec + UniFi security stack
#
# Usage:
#   curl -sSL https://raw.githubusercontent.com/wolffcatskyy/crowdsec-unifi-suite/main/install.sh | bash
#
# Components:
#   ON UNIFI DEVICE:
#     - CrowdSec Engine
#     - UniFi Parser Collection (wolffcatskyy/unifi)
#     - UniFi Bouncer (firewall ipset enforcement)
#
#   ON SERVER (Docker):
#     - Sidecar Proxy (decision prioritization + capping)
#     - Blocklist Import (external threat feed importer)
#     - AbuseIPDB Reporter (optional abuse reporting)
#

# ---------------------------------------------------------------------------
# curl-pipe stdin fix
# When invoked via `curl | bash`, stdin is the curl stream, not the terminal.
# We save the script to a temp file and re-exec with /dev/tty as stdin.
# ---------------------------------------------------------------------------
if [ ! -t 0 ]; then
    SELF_URL="https://raw.githubusercontent.com/wolffcatskyy/crowdsec-unifi-suite/main/install.sh"
    TMPFILE=$(mktemp /tmp/unifi-suite-install.XXXXXX.sh)
    trap 'rm -f "$TMPFILE"' EXIT
    cat > "$TMPFILE"
    exec bash "$TMPFILE" "$@" </dev/tty
fi

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

# ---------------------------------------------------------------------------
# Global Variables
# ---------------------------------------------------------------------------
VERSION="1.0.2"
GITHUB_RAW="https://raw.githubusercontent.com/wolffcatskyy"
GITHUB_BASE="https://github.com/wolffcatskyy"
LOG_FILE="$HOME/.crowdsec-suite-install.log"

# Detection results (populated by detect_environment)
IS_UNIFI=false
IS_DOCKER_HOST=false
DEVICE_MODEL=""
DEVICE_TIER=""
ARCH=""
IPSET_CAPACITY=0

# Component status
CS_INSTALLED=false
PARSER_INSTALLED=false
BOUNCER_INSTALLED=false
SIDECAR_INSTALLED=false
BLOCKLIST_INSTALLED=false
ABUSEIPDB_ENABLED=false

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
log_init() {
    mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
    echo "" >> "$LOG_FILE"
    echo "=======================================" >> "$LOG_FILE"
    echo "Session started: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
    echo "=======================================" >> "$LOG_FILE"
}

log() {
    echo "[$(date '+%H:%M:%S')] $1" >> "$LOG_FILE" 2>/dev/null
}

# ---------------------------------------------------------------------------
# Display Helpers
# ---------------------------------------------------------------------------
print_line() {
    echo -e "${DIM}_______________________________________________________________${NC}"
}

print_status() {
    local label="$1"
    local status="$2"
    case "$status" in
        installed)  echo -e "  ${GREEN}+${NC} ${label}  ${GREEN}installed${NC}" ;;
        partial)    echo -e "  ${YELLOW}~${NC} ${label}  ${YELLOW}partial${NC}" ;;
        missing)    echo -e "  ${RED}-${NC} ${label}  ${DIM}not installed${NC}" ;;
        enabled)    echo -e "  ${GREEN}+${NC} ${label}  ${GREEN}enabled${NC}" ;;
        disabled)   echo -e "  ${DIM}-${NC} ${label}  ${DIM}disabled${NC}" ;;
    esac
}

msg_info() {
    echo -e "  ${BLUE}i${NC} $1"
}

msg_ok() {
    echo -e "  ${GREEN}+${NC} $1"
}

msg_warn() {
    echo -e "  ${YELLOW}!${NC} $1"
}

msg_err() {
    echo -e "  ${RED}x${NC} $1"
}

msg_dim() {
    echo -e "  ${DIM}$1${NC}"
}

press_any_key() {
    echo ""
    echo -e "  ${DIM}Press any key to continue...${NC}"
    read -n 1 -s </dev/tty
}

# Show Retry / Skip / Abort prompt on failure
# Returns: 0=retry, 1=skip, 2=abort
error_prompt() {
    local component="$1"
    echo ""
    msg_err "$component installation failed"
    echo ""
    echo -e "  ${BOLD}[R]${NC}etry  ${BOLD}[S]${NC}kip  ${BOLD}[A]${NC}bort"
    echo ""
    while true; do
        read -n 1 -s choice </dev/tty
        case "$choice" in
            r|R) return 0 ;;
            s|S) return 1 ;;
            a|A) return 2 ;;
        esac
    done
}

# ---------------------------------------------------------------------------
# Banner
# ---------------------------------------------------------------------------
show_banner() {
    clear
    echo ""
    echo -e "${CYAN}${BOLD}"
    echo "    ___                     _  ___            "
    echo "   / __\ __ _____      __ | |/ __\ ___  ___  "
    echo "  / / | '__/ _ \ \ /\ / / | / /  / _ \/ __|"
    echo " / /__| | | (_) \ V  V /  | / /__| __|  (__ "
    echo " \____/_|  \___/ \_/\_/ |___|____/\___|\___|"
    echo ""
    echo -e "            ${BLUE}U n i F i   S u i t e${NC}"
    echo ""
    echo -e "  ${DIM}v${VERSION}               Detect -> Decide -> Enforce${NC}"
    print_line
    echo ""
}

# ---------------------------------------------------------------------------
# Environment Detection
# ---------------------------------------------------------------------------
detect_environment() {
    log "Starting environment detection"

    # Architecture
    ARCH=$(uname -m)
    if [ "$ARCH" = "mips64" ]; then
        show_banner
        msg_err "USG devices (mips64) are not supported."
        msg_info "This suite requires UniFi OS 2.x+ (arm64/amd64)."
        echo ""
        exit 1
    fi
    log "Architecture: $ARCH"

    # UniFi device detection
    if [ -f /proc/ubnthal/system.info ] || command -v ubnt-device-info >/dev/null 2>&1 || [ -d /data/unifi-core ]; then
        IS_UNIFI=true
        log "UniFi device detected"
    fi

    # Device model
    if command -v ubnt-device-info >/dev/null 2>&1; then
        DEVICE_MODEL=$(ubnt-device-info model 2>/dev/null)
    elif [ -f /proc/ubnthal/system.info ]; then
        DEVICE_MODEL=$(grep -i shortname /proc/ubnthal/system.info 2>/dev/null | cut -d= -f2 | tr -d ' ')
    fi
    log "Device model: ${DEVICE_MODEL:-unknown}"

    # ipset capacity by model
    case "$DEVICE_MODEL" in
        EFG|UXG-Enterprise)
            IPSET_CAPACITY=80000; DEVICE_TIER="enterprise" ;;
        UDM-SE|UDMSE|UDM-Pro*|UDMPRO*|UDW|UCG-*|UXG-*)
            IPSET_CAPACITY=50000; DEVICE_TIER="pro" ;;
        UDM|UDR|UDR7|UX7)
            IPSET_CAPACITY=15000; DEVICE_TIER="consumer" ;;
        *)
            IPSET_CAPACITY=10000; DEVICE_TIER="unknown" ;;
    esac
    log "Device tier: $DEVICE_TIER, ipset capacity: $IPSET_CAPACITY"

    # Docker host detection
    if command -v docker >/dev/null 2>&1; then
        if docker info >/dev/null 2>&1; then
            IS_DOCKER_HOST=true
            log "Docker available"
        fi
    fi

    # Component detection
    if command -v cscli >/dev/null 2>&1; then
        CS_INSTALLED=true
        log "CrowdSec engine found"
    fi

    if [ -f /etc/crowdsec/hub/collections/wolffcatskyy/unifi.yaml ] || \
       cscli collections list 2>/dev/null | grep -q "wolffcatskyy/unifi"; then
        PARSER_INSTALLED=true
        log "UniFi parser collection found"
    fi

    if [ -f /data/crowdsec-bouncer/crowdsec-firewall-bouncer ]; then
        BOUNCER_INSTALLED=true
        log "UniFi bouncer found"
    fi

    if docker ps 2>/dev/null | grep -q crowdsec-sidecar; then
        SIDECAR_INSTALLED=true
        log "Sidecar proxy running"
    fi

    if docker ps -a 2>/dev/null | grep -q crowdsec-blocklist-import; then
        BLOCKLIST_INSTALLED=true
        log "Blocklist import found"
    fi

    # AbuseIPDB check
    if [ "$SIDECAR_INSTALLED" = true ]; then
        if docker inspect crowdsec-sidecar 2>/dev/null | grep -q '"ABUSEIPDB_REPORT_ENABLED=true"'; then
            ABUSEIPDB_ENABLED=true
            log "AbuseIPDB reporting enabled"
        fi
    fi
}

# ---------------------------------------------------------------------------
# Root Check
# ---------------------------------------------------------------------------
check_root() {
    if [ "$(id -u)" -ne 0 ]; then
        msg_err "This installer must be run as root."
        msg_info "Try: sudo bash install.sh"
        echo ""
        exit 1
    fi
}

# ---------------------------------------------------------------------------
# Main Menu
# ---------------------------------------------------------------------------
show_main_menu() {
    echo -e "  ${BOLD}Main Menu${NC}"
    echo ""
    echo -e "  ${BOLD}[1]${NC}  Install Full Suite  ${DIM}(recommended)${NC}"
    echo -e "  ${BOLD}[2]${NC}  Install Individual Components"
    echo -e "  ${BOLD}[3]${NC}  Check Installation Status"
    echo -e "  ${BOLD}[4]${NC}  Update Components"
    echo -e "  ${BOLD}[5]${NC}  Uninstall Components"
    echo ""
    print_line
    echo -e "  ${BOLD}[0]${NC}  Exit"
    echo ""

    read -n 1 -s choice </dev/tty
    case "$choice" in
        1) install_full_suite ;;
        2) show_components_menu ;;
        3) show_status_screen ;;
        4) show_update_menu ;;
        5) show_uninstall_menu ;;
        0|q) clear; exit 0 ;;
        *) ;;
    esac
}

# ---------------------------------------------------------------------------
# Components Menu (context-aware)
# ---------------------------------------------------------------------------
show_components_menu() {
    show_banner
    echo -e "  ${BOLD}Install Individual Components${NC}"
    echo ""

    if [ "$IS_UNIFI" = true ]; then
        echo -e "  ${DIM}-- UniFi Device Components --${NC}"
        echo -e "  ${BOLD}[1]${NC}  CrowdSec Engine"
        echo -e "  ${BOLD}[2]${NC}  UniFi Parser Collection"
        echo -e "  ${BOLD}[3]${NC}  UniFi Bouncer"
        echo ""
    fi

    if [ "$IS_DOCKER_HOST" = true ]; then
        echo -e "  ${DIM}-- Server Components (Docker) --${NC}"
        echo -e "  ${BOLD}[4]${NC}  Sidecar Proxy"
        echo -e "  ${BOLD}[5]${NC}  Blocklist Import"
        echo -e "  ${BOLD}[6]${NC}  AbuseIPDB Reporter"
        echo ""
    fi

    if [ "$IS_UNIFI" = false ] && [ "$IS_DOCKER_HOST" = false ]; then
        msg_warn "No UniFi device or Docker detected."
        msg_info "Showing all options."
        echo ""
        echo -e "  ${BOLD}[1]${NC}  CrowdSec Engine"
        echo -e "  ${BOLD}[2]${NC}  UniFi Parser Collection"
        echo -e "  ${BOLD}[3]${NC}  UniFi Bouncer"
        echo -e "  ${BOLD}[4]${NC}  Sidecar Proxy"
        echo -e "  ${BOLD}[5]${NC}  Blocklist Import"
        echo -e "  ${BOLD}[6]${NC}  AbuseIPDB Reporter"
        echo ""
    fi

    print_line
    echo -e "  ${BOLD}[B]${NC}  Back to main menu"
    echo ""

    read -n 1 -s choice </dev/tty
    case "$choice" in
        1) run_install install_crowdsec "CrowdSec Engine" ;;
        2) run_install install_parser "UniFi Parser" ;;
        3) run_install install_bouncer "UniFi Bouncer" ;;
        4) run_install install_sidecar "Sidecar Proxy" ;;
        5) run_install install_blocklist "Blocklist Import" ;;
        6) run_install configure_abuseipdb "AbuseIPDB Reporter" ;;
        b|B) return ;;
        *) show_components_menu ;;
    esac
}

# ---------------------------------------------------------------------------
# Status Screen
# ---------------------------------------------------------------------------
show_status_screen() {
    # Re-detect before showing status
    detect_environment

    show_banner
    echo -e "  ${BOLD}Installation Status${NC}"
    echo ""

    echo -e "  ${DIM}-- Device Info --${NC}"
    if [ "$IS_UNIFI" = true ]; then
        msg_info "Device:      ${DEVICE_MODEL:-Unknown UniFi}"
        msg_info "Tier:        ${DEVICE_TIER}"
        msg_info "ipset cap:   ${IPSET_CAPACITY}"
    else
        msg_info "Device:      Non-UniFi host"
    fi
    msg_info "Arch:        ${ARCH}"
    msg_info "Docker:      $([ "$IS_DOCKER_HOST" = true ] && echo "available" || echo "not found")"
    echo ""

    echo -e "  ${DIM}-- Components --${NC}"
    [ "$CS_INSTALLED" = true ]        && print_status "CrowdSec Engine       " "installed" \
                                      || print_status "CrowdSec Engine       " "missing"
    [ "$PARSER_INSTALLED" = true ]    && print_status "UniFi Parser          " "installed" \
                                      || print_status "UniFi Parser          " "missing"
    [ "$BOUNCER_INSTALLED" = true ]   && print_status "UniFi Bouncer         " "installed" \
                                      || print_status "UniFi Bouncer         " "missing"
    [ "$SIDECAR_INSTALLED" = true ]   && print_status "Sidecar Proxy         " "installed" \
                                      || print_status "Sidecar Proxy         " "missing"
    [ "$BLOCKLIST_INSTALLED" = true ] && print_status "Blocklist Import      " "installed" \
                                      || print_status "Blocklist Import      " "missing"
    [ "$ABUSEIPDB_ENABLED" = true ]   && print_status "AbuseIPDB Reporter    " "enabled" \
                                      || print_status "AbuseIPDB Reporter    " "disabled"
    echo ""

    # Show bouncer service status if installed
    if [ "$BOUNCER_INSTALLED" = true ]; then
        echo -e "  ${DIM}-- Bouncer Service --${NC}"
        if systemctl is-active crowdsec-firewall-bouncer >/dev/null 2>&1; then
            msg_ok "Service: running"
        else
            msg_warn "Service: stopped"
        fi
        local ipset_count
        ipset_count=$(ipset list crowdsec-blacklists 2>/dev/null | grep -c "^[0-9]" || echo "0")
        msg_info "Blocked IPs: ${ipset_count}"
        echo ""
    fi

    print_line
    echo -e "  ${BOLD}[R]${NC}  Refresh    ${BOLD}[B]${NC}  Back"
    echo ""

    read -n 1 -s choice </dev/tty
    case "$choice" in
        r|R) show_status_screen ;;
        b|B) return ;;
        *) return ;;
    esac
}

# ---------------------------------------------------------------------------
# Install Runner (wraps install functions with error handling)
# ---------------------------------------------------------------------------
run_install() {
    local func="$1"
    local name="$2"

    show_banner
    echo -e "  ${BOLD}Installing: ${name}${NC}"
    echo ""

    while true; do
        if "$func"; then
            echo ""
            msg_ok "${name} installed successfully."
            log "${name} installed successfully"
            press_any_key
            return 0
        else
            error_prompt "$name"
            local result=$?
            if [ $result -eq 0 ]; then
                # Retry
                show_banner
                echo -e "  ${BOLD}Retrying: ${name}${NC}"
                echo ""
                continue
            elif [ $result -eq 1 ]; then
                # Skip
                msg_warn "Skipping ${name}."
                log "${name} skipped by user"
                press_any_key
                return 1
            else
                # Abort
                msg_err "Installation aborted."
                log "Installation aborted during ${name}"
                press_any_key
                return 2
            fi
        fi
    done
}

# ---------------------------------------------------------------------------
# Install: Full Suite
# ---------------------------------------------------------------------------
install_full_suite() {
    show_banner
    echo -e "  ${BOLD}Full Suite Installation${NC}"
    echo ""
    msg_info "This will install all components appropriate for your device."
    echo ""

    if [ "$IS_UNIFI" = true ]; then
        msg_info "Detected UniFi device. Installing:"
        msg_dim "  1. CrowdSec Engine"
        msg_dim "  2. UniFi Parser Collection"
        msg_dim "  3. UniFi Bouncer"
    elif [ "$IS_DOCKER_HOST" = true ]; then
        msg_info "Detected Docker host. Installing:"
        msg_dim "  1. CrowdSec Engine (Docker)"
        msg_dim "  2. Sidecar Proxy"
        msg_dim "  3. Blocklist Import"
    else
        msg_info "Installing all available components:"
        msg_dim "  1. CrowdSec Engine"
        msg_dim "  2. UniFi Parser Collection"
        msg_dim "  3. UniFi Bouncer"
    fi

    echo ""
    echo -e "  ${BOLD}Proceed? [Y/n]${NC} "
    read -n 1 -s confirm </dev/tty
    echo ""
    if [ "$confirm" = "n" ] || [ "$confirm" = "N" ]; then
        return
    fi

    local abort=false

    if [ "$IS_UNIFI" = true ]; then
        # UniFi device: engine + parser + bouncer
        if [ "$CS_INSTALLED" = false ]; then
            run_install install_crowdsec "CrowdSec Engine" || { [ $? -eq 2 ] && abort=true; }
        else
            msg_ok "CrowdSec Engine already installed, skipping."
        fi

        if [ "$abort" = false ] && [ "$PARSER_INSTALLED" = false ]; then
            run_install install_parser "UniFi Parser" || { [ $? -eq 2 ] && abort=true; }
        elif [ "$abort" = false ]; then
            msg_ok "UniFi Parser already installed, skipping."
        fi

        if [ "$abort" = false ] && [ "$BOUNCER_INSTALLED" = false ]; then
            run_install install_bouncer "UniFi Bouncer" || { [ $? -eq 2 ] && abort=true; }
        elif [ "$abort" = false ]; then
            msg_ok "UniFi Bouncer already installed, skipping."
        fi

    elif [ "$IS_DOCKER_HOST" = true ]; then
        # Docker host: engine (docker) + sidecar + blocklist
        if [ "$CS_INSTALLED" = false ]; then
            run_install install_crowdsec "CrowdSec Engine" || { [ $? -eq 2 ] && abort=true; }
        else
            msg_ok "CrowdSec Engine already installed, skipping."
        fi

        if [ "$abort" = false ] && [ "$SIDECAR_INSTALLED" = false ]; then
            run_install install_sidecar "Sidecar Proxy" || { [ $? -eq 2 ] && abort=true; }
        elif [ "$abort" = false ]; then
            msg_ok "Sidecar Proxy already installed, skipping."
        fi

        if [ "$abort" = false ] && [ "$BLOCKLIST_INSTALLED" = false ]; then
            run_install install_blocklist "Blocklist Import" || { [ $? -eq 2 ] && abort=true; }
        elif [ "$abort" = false ]; then
            msg_ok "Blocklist Import already installed, skipping."
        fi

    else
        # Unknown device: try engine + parser + bouncer
        if [ "$CS_INSTALLED" = false ]; then
            run_install install_crowdsec "CrowdSec Engine" || { [ $? -eq 2 ] && abort=true; }
        else
            msg_ok "CrowdSec Engine already installed, skipping."
        fi

        if [ "$abort" = false ] && [ "$PARSER_INSTALLED" = false ]; then
            run_install install_parser "UniFi Parser" || { [ $? -eq 2 ] && abort=true; }
        elif [ "$abort" = false ]; then
            msg_ok "UniFi Parser already installed, skipping."
        fi

        if [ "$abort" = false ] && [ "$BOUNCER_INSTALLED" = false ]; then
            run_install install_bouncer "UniFi Bouncer" || { [ $? -eq 2 ] && abort=true; }
        elif [ "$abort" = false ]; then
            msg_ok "UniFi Bouncer already installed, skipping."
        fi
    fi

    if [ "$abort" = false ]; then
        # Refresh detection
        detect_environment

        show_banner
        echo -e "  ${BOLD}${GREEN}Installation Complete${NC}"
        echo ""
        show_post_install_summary
    fi

    press_any_key
}

# ---------------------------------------------------------------------------
# Post-Install Summary
# ---------------------------------------------------------------------------
show_post_install_summary() {
    echo -e "  ${DIM}-- Installed Components --${NC}"
    [ "$CS_INSTALLED" = true ]        && msg_ok "CrowdSec Engine"
    [ "$PARSER_INSTALLED" = true ]    && msg_ok "UniFi Parser Collection"
    [ "$BOUNCER_INSTALLED" = true ]   && msg_ok "UniFi Bouncer"
    [ "$SIDECAR_INSTALLED" = true ]   && msg_ok "Sidecar Proxy"
    [ "$BLOCKLIST_INSTALLED" = true ] && msg_ok "Blocklist Import"
    [ "$ABUSEIPDB_ENABLED" = true ]   && msg_ok "AbuseIPDB Reporter"
    echo ""

    if [ "$BOUNCER_INSTALLED" = true ]; then
        echo -e "  ${DIM}-- Next Steps --${NC}"
        msg_info "1. Verify bouncer config:"
        msg_dim "   cat /data/crowdsec-bouncer/crowdsec-firewall-bouncer.yaml"
        msg_info "2. Check bouncer status:"
        msg_dim "   systemctl status crowdsec-firewall-bouncer"
        msg_info "3. View blocked IPs:"
        msg_dim "   ipset list crowdsec-blacklists | head -20"
        echo ""
    fi

    if [ "$SIDECAR_INSTALLED" = true ]; then
        echo -e "  ${DIM}-- Sidecar Info --${NC}"
        msg_info "Bouncer connects to sidecar on port 8084"
        msg_info "Set api_url in bouncer config to:"
        msg_dim "   http://$(hostname -I 2>/dev/null | awk '{print $1}'):8084/"
        echo ""
    fi

    echo -e "  ${DIM}Documentation: ${GITHUB_BASE}/crowdsec-unifi-suite${NC}"
    echo ""
}

# ---------------------------------------------------------------------------
# Install: CrowdSec Engine
# ---------------------------------------------------------------------------
install_crowdsec() {
    if [ "$CS_INSTALLED" = true ]; then
        msg_ok "CrowdSec is already installed."
        local cs_ver
        cs_ver=$(cscli version 2>/dev/null | grep -oP 'version:\s*\Kv[\d.]+' || echo "unknown")
        msg_info "Version: ${cs_ver}"
        return 0
    fi

    msg_info "Installing CrowdSec Engine..."
    log "Installing CrowdSec Engine"

    # The official installer configures the package repository; package
    # installation is a separate step. Capture each pipeline's real status so
    # tee cannot mask a failed curl, shell, or package-manager command.
    curl -fsSL https://install.crowdsec.net | bash 2>&1 | tee -a "$LOG_FILE"
    if [ "${PIPESTATUS[0]}" -ne 0 ] || [ "${PIPESTATUS[1]}" -ne 0 ]; then
        msg_err "CrowdSec repository setup failed."
        log "CrowdSec repository setup failed"
        return 1
    fi

    if command -v apt-get >/dev/null 2>&1; then
        apt-get update 2>&1 | tee -a "$LOG_FILE"
        [ "${PIPESTATUS[0]}" -eq 0 ] || return 1
        apt-get install -y crowdsec 2>&1 | tee -a "$LOG_FILE"
        [ "${PIPESTATUS[0]}" -eq 0 ] || return 1
    elif command -v yum >/dev/null 2>&1; then
        yum install -y crowdsec 2>&1 | tee -a "$LOG_FILE"
        [ "${PIPESTATUS[0]}" -eq 0 ] || return 1
    elif command -v apk >/dev/null 2>&1; then
        apk add crowdsec 2>&1 | tee -a "$LOG_FILE"
        [ "${PIPESTATUS[0]}" -eq 0 ] || return 1
    else
        msg_err "No supported package manager found (apt-get, yum, or apk)."
        return 1
    fi

    hash -r
    # Verify installation
    if ! command -v cscli >/dev/null 2>&1; then
        msg_err "CrowdSec binary not found after installation."
        return 1
    fi

    CS_INSTALLED=true
    msg_ok "CrowdSec Engine installed."

    # Offer console enrollment
    echo ""
    echo -e "  ${BOLD}Enroll in CrowdSec Console? [y/N]${NC} "
    read -n 1 -s enroll </dev/tty
    echo ""

    if [ "$enroll" = "y" ] || [ "$enroll" = "Y" ]; then
        echo -e "  Enter enrollment key from ${CYAN}https://app.crowdsec.net${NC}"
        echo -n "  Key: "
        read -r enroll_key </dev/tty
        if [ -n "$enroll_key" ]; then
            if cscli console enroll "$enroll_key" 2>&1 | tee -a "$LOG_FILE"; then
                msg_ok "Enrolled in CrowdSec Console."
            else
                msg_warn "Enrollment failed. You can enroll later with:"
                msg_dim "  cscli console enroll <key>"
            fi
        fi
    fi

    # Install base collections
    msg_info "Installing base collections..."
    cscli collections install crowdsecurity/linux 2>&1 | tee -a "$LOG_FILE" || true
    cscli collections install crowdsecurity/sshd 2>&1 | tee -a "$LOG_FILE" || true

    return 0
}

# ---------------------------------------------------------------------------
# Install: UniFi Parser Collection
# ---------------------------------------------------------------------------
install_parser() {
    if [ "$PARSER_INSTALLED" = true ]; then
        msg_ok "UniFi Parser Collection is already installed."
        return 0
    fi

    if [ "$CS_INSTALLED" = false ]; then
        msg_err "CrowdSec Engine is required but not installed."
        msg_info "Install CrowdSec first (option 1)."
        return 1
    fi

    msg_info "Installing UniFi Parser Collection..."
    log "Installing UniFi Parser Collection"

    # Try Hub install first
    if cscli collections install wolffcatskyy/unifi 2>&1 | tee -a "$LOG_FILE"; then
        PARSER_INSTALLED=true
        msg_ok "Parser collection installed from CrowdSec Hub."

        # Reload CrowdSec to pick up new parsers
        msg_info "Reloading CrowdSec..."
        systemctl reload crowdsec 2>/dev/null || cscli hub update 2>/dev/null || true
        return 0
    fi

    # Fallback: install from GitHub
    msg_warn "Hub install failed. Trying direct install from GitHub..."
    if curl -sSL "${GITHUB_RAW}/crowdsec-unifi-parser/main/install.sh" | bash 2>&1 | tee -a "$LOG_FILE"; then
        PARSER_INSTALLED=true
        msg_ok "Parser collection installed from GitHub."
        return 0
    fi

    msg_err "Parser collection installation failed."
    return 1
}

# ---------------------------------------------------------------------------
# Install: UniFi Bouncer
# ---------------------------------------------------------------------------
install_bouncer() {
    if [ "$BOUNCER_INSTALLED" = true ]; then
        msg_ok "UniFi Bouncer is already installed."
        return 0
    fi

    msg_info "Installing UniFi Bouncer..."
    log "Installing UniFi Bouncer"

    # Delegate to the bouncer's own bootstrap script
    if ! curl -sSL "${GITHUB_RAW}/crowdsec-unifi-bouncer/main/bootstrap.sh" | bash 2>&1 | tee -a "$LOG_FILE"; then
        msg_err "Bouncer bootstrap failed."
        return 1
    fi

    # Prompt for LAPI configuration
    echo ""
    msg_info "Configure bouncer connection to CrowdSec LAPI."
    echo ""

    local default_api_url="http://192.168.1.1:8080/"
    echo -n "  LAPI URL [${default_api_url}]: "
    read -r api_url </dev/tty
    api_url="${api_url:-$default_api_url}"

    echo ""
    msg_info "Generate a bouncer API key on your CrowdSec host:"
    msg_dim "  cscli bouncers add unifi-bouncer"
    echo ""
    echo -n "  Bouncer API Key: "
    read -r api_key </dev/tty

    if [ -z "$api_key" ]; then
        msg_warn "No API key provided. You must configure it manually:"
        msg_dim "  Edit /data/crowdsec-bouncer/crowdsec-firewall-bouncer.yaml"
        BOUNCER_INSTALLED=true
        return 0
    fi

    # Write configuration
    local config_file="/data/crowdsec-bouncer/crowdsec-firewall-bouncer.yaml"
    if [ -f "${config_file}.example" ] && [ ! -f "$config_file" ]; then
        cp "${config_file}.example" "$config_file"
    fi

    if [ -f "$config_file" ]; then
        sed -i "s|api_url:.*|api_url: ${api_url}|" "$config_file"
        sed -i "s|api_key:.*|api_key: ${api_key}|" "$config_file"
        msg_ok "Configuration written to ${config_file}"
    else
        msg_warn "Config file not found at ${config_file}"
        msg_info "Create it manually with api_url and api_key set."
    fi

    # Enable and start the service
    if systemctl is-enabled crowdsec-firewall-bouncer >/dev/null 2>&1; then
        msg_info "Starting bouncer service..."
        systemctl restart crowdsec-firewall-bouncer 2>&1 | tee -a "$LOG_FILE" || true
    else
        msg_info "Enabling bouncer service..."
        systemctl enable crowdsec-firewall-bouncer 2>&1 | tee -a "$LOG_FILE" || true
        systemctl start crowdsec-firewall-bouncer 2>&1 | tee -a "$LOG_FILE" || true
    fi

    # Verify
    sleep 2
    if systemctl is-active crowdsec-firewall-bouncer >/dev/null 2>&1; then
        msg_ok "Bouncer service is running."
    else
        msg_warn "Bouncer service may not have started. Check with:"
        msg_dim "  systemctl status crowdsec-firewall-bouncer"
    fi

    BOUNCER_INSTALLED=true
    return 0
}

# ---------------------------------------------------------------------------
# Install: Sidecar Proxy
# ---------------------------------------------------------------------------
install_sidecar() {
    if [ "$SIDECAR_INSTALLED" = true ]; then
        msg_ok "Sidecar Proxy is already running."
        return 0
    fi

    if [ "$IS_DOCKER_HOST" = false ]; then
        msg_err "Docker is required for the Sidecar Proxy."
        msg_info "Install Docker first or run this on a Docker-capable host."
        return 1
    fi

    msg_info "Installing Sidecar Proxy..."
    log "Installing Sidecar Proxy"

    local install_dir="/opt/crowdsec-suite"
    mkdir -p "$install_dir"

    # Download compose and config files
    msg_info "Downloading configuration files..."
    local suite_raw="${GITHUB_RAW}/crowdsec-unifi-suite/main"

    if ! curl -sSL "${suite_raw}/docker-compose.yml" -o "${install_dir}/docker-compose.yml"; then
        msg_err "Failed to download docker-compose.yml"
        return 1
    fi

    if ! curl -sSL "${suite_raw}/sidecar-config.yaml" -o "${install_dir}/sidecar-config.yaml"; then
        msg_err "Failed to download sidecar-config.yaml"
        return 1
    fi

    # Prompt for bouncer API key
    echo ""
    msg_info "The sidecar needs a CrowdSec bouncer API key."
    msg_dim "  Generate one with: docker exec crowdsec cscli bouncers add crowdsec-sidecar"
    echo ""
    echo -n "  Sidecar Bouncer API Key: "
    read -r sidecar_key </dev/tty

    if [ -n "$sidecar_key" ]; then
        sed -i "s|upstream_lapi_key:.*|upstream_lapi_key: \"${sidecar_key}\"|" "${install_dir}/sidecar-config.yaml"
    else
        msg_warn "No API key provided. Edit sidecar-config.yaml before starting."
    fi

    # Device tier selection for max_decisions
    echo ""
    msg_info "Select your UniFi device tier for decision capping:"
    echo ""
    echo -e "  ${BOLD}[1]${NC}  Enterprise  ${DIM}(EFG / UXG-Enterprise)${NC}    ${CYAN}80,000${NC}"
    echo -e "  ${BOLD}[2]${NC}  Pro         ${DIM}(UDM-SE / UDM-Pro / UCG)${NC}  ${CYAN}50,000${NC}"
    echo -e "  ${BOLD}[3]${NC}  Consumer    ${DIM}(UDM / UDR)${NC}              ${CYAN}15,000${NC}"
    echo -e "  ${BOLD}[4]${NC}  Custom"
    echo ""

    local max_decisions=45000
    read -n 1 -s tier_choice </dev/tty
    echo ""

    case "$tier_choice" in
        1) max_decisions=75000 ;;
        2) max_decisions=45000 ;;
        3) max_decisions=13000 ;;
        4)
            echo -n "  Enter max decisions: "
            read -r max_decisions </dev/tty
            if ! echo "$max_decisions" | grep -qE '^[0-9]+$'; then
                msg_warn "Invalid number, using default 45000."
                max_decisions=45000
            fi
            ;;
        *) msg_info "Using default: 45000" ;;
    esac

    sed -i "s|max_decisions:.*|max_decisions: ${max_decisions}|" "${install_dir}/sidecar-config.yaml"
    msg_ok "max_decisions set to ${max_decisions}"

    # Create .env file
    if [ ! -f "${install_dir}/.env" ]; then
        cat > "${install_dir}/.env" <<ENVEOF
TZ=America/New_York
CROWDSEC_HOSTNAME=$(hostname)
CROWDSEC_ENROLL_KEY=
ABUSEIPDB_API_KEY=
ABUSEIPDB_REPORT_ENABLED=false
BLOCKLIST_MACHINE_ID=blocklist-import
BLOCKLIST_MACHINE_PASSWORD=
BLOCKLIST_BOUNCER_KEY=
BLOCKLIST_DECISION_DURATION=24h
BLOCKLIST_MAX_DECISIONS=0
ENABLE_IPSUM=true
ENABLE_ABUSE_IPDB=true
ENABLE_SPAMHAUS=true
ENABLE_TOR=false
ENABLE_SCANNERS=false
LOG_LEVEL=INFO
ENVEOF
        msg_info "Created ${install_dir}/.env — edit with your credentials."
    fi

    # Start sidecar only (not the full stack)
    msg_info "Starting Sidecar Proxy..."
    cd "$install_dir"
    if docker compose up -d crowdsec-sidecar 2>&1 | tee -a "$LOG_FILE"; then
        sleep 3
        if docker ps | grep -q crowdsec-sidecar; then
            SIDECAR_INSTALLED=true
            msg_ok "Sidecar Proxy is running on port 8084."
        else
            msg_warn "Container started but may not be healthy yet."
            msg_dim "  Check with: docker logs crowdsec-sidecar"
        fi
    else
        msg_err "Failed to start Sidecar Proxy."
        return 1
    fi

    return 0
}

# ---------------------------------------------------------------------------
# Install: Blocklist Import
# ---------------------------------------------------------------------------
install_blocklist() {
    if [ "$BLOCKLIST_INSTALLED" = true ]; then
        msg_ok "Blocklist Import is already installed."
        return 0
    fi

    msg_info "Installing Blocklist Import..."
    log "Installing Blocklist Import"

    if [ "$IS_DOCKER_HOST" = true ]; then
        # Docker install
        local install_dir="/opt/crowdsec-suite"
        mkdir -p "$install_dir"

        # Make sure compose file exists
        if [ ! -f "${install_dir}/docker-compose.yml" ]; then
            msg_info "Downloading docker-compose.yml..."
            curl -sSL "${GITHUB_RAW}/crowdsec-unifi-suite/main/docker-compose.yml" \
                -o "${install_dir}/docker-compose.yml" || {
                msg_err "Failed to download docker-compose.yml"
                return 1
            }
        fi

        # Create .env if missing
        if [ ! -f "${install_dir}/.env" ]; then
            msg_warn "No .env file found."
            msg_info "You need CrowdSec machine credentials for blocklist-import."
            echo ""
            echo -n "  Machine ID [blocklist-import]: "
            read -r bl_machine_id </dev/tty
            bl_machine_id="${bl_machine_id:-blocklist-import}"

            echo -n "  Machine Password: "
            read -r bl_machine_pass </dev/tty

            echo -n "  Bouncer API Key (for dedup): "
            read -r bl_bouncer_key </dev/tty

            cat > "${install_dir}/.env" <<ENVEOF
TZ=America/New_York
BLOCKLIST_MACHINE_ID=${bl_machine_id}
BLOCKLIST_MACHINE_PASSWORD=${bl_machine_pass}
BLOCKLIST_BOUNCER_KEY=${bl_bouncer_key}
BLOCKLIST_DECISION_DURATION=24h
BLOCKLIST_MAX_DECISIONS=0
ENABLE_IPSUM=true
ENABLE_ABUSE_IPDB=true
ENABLE_SPAMHAUS=true
ENABLE_TOR=false
ENABLE_SCANNERS=false
LOG_LEVEL=INFO
ENVEOF
        fi

        msg_info "Starting Blocklist Import container..."
        cd "$install_dir"
        if docker compose up -d blocklist-import 2>&1 | tee -a "$LOG_FILE"; then
            sleep 3
            if docker ps -a | grep -q crowdsec-blocklist-import; then
                BLOCKLIST_INSTALLED=true
                msg_ok "Blocklist Import container started."
            else
                msg_warn "Container may not have started correctly."
                msg_dim "  Check with: docker logs crowdsec-blocklist-import"
            fi
        else
            msg_err "Failed to start Blocklist Import."
            return 1
        fi
    else
        # Non-Docker: use the standalone installer
        msg_info "Installing via standalone installer..."
        if curl -sSL "${GITHUB_RAW}/crowdsec-blocklist-import/main/install.sh" | bash 2>&1 | tee -a "$LOG_FILE"; then
            BLOCKLIST_INSTALLED=true
            msg_ok "Blocklist Import installed."
        else
            msg_err "Blocklist Import installation failed."
            return 1
        fi
    fi

    return 0
}

# ---------------------------------------------------------------------------
# Configure: AbuseIPDB Reporter
# ---------------------------------------------------------------------------
configure_abuseipdb() {
    if [ "$SIDECAR_INSTALLED" = false ]; then
        msg_err "Sidecar Proxy must be installed first."
        msg_info "AbuseIPDB reporting is a feature of the Sidecar Proxy."
        return 1
    fi

    msg_info "Configuring AbuseIPDB Reporter..."
    log "Configuring AbuseIPDB"

    echo ""
    msg_info "Get a free API key at: ${CYAN}https://www.abuseipdb.com/account/api${NC}"
    msg_dim "  Free tier: 100 reports/day"
    echo ""
    echo -n "  AbuseIPDB API Key: "
    read -r abuse_key </dev/tty

    if [ -z "$abuse_key" ]; then
        msg_warn "No API key provided. AbuseIPDB not configured."
        return 1
    fi

    local install_dir="/opt/crowdsec-suite"
    if [ -f "${install_dir}/.env" ]; then
        sed -i "s|ABUSEIPDB_API_KEY=.*|ABUSEIPDB_API_KEY=${abuse_key}|" "${install_dir}/.env"
        sed -i "s|ABUSEIPDB_REPORT_ENABLED=.*|ABUSEIPDB_REPORT_ENABLED=true|" "${install_dir}/.env"
    else
        msg_err ".env file not found at ${install_dir}/.env"
        return 1
    fi

    # Recreate sidecar container with new env
    msg_info "Restarting Sidecar Proxy with AbuseIPDB enabled..."
    cd "$install_dir"
    if docker compose up -d crowdsec-sidecar 2>&1 | tee -a "$LOG_FILE"; then
        ABUSEIPDB_ENABLED=true
        msg_ok "AbuseIPDB Reporter enabled."
        msg_info "Reports will be sent for locally-detected malicious IPs."
    else
        msg_err "Failed to restart Sidecar Proxy."
        return 1
    fi

    return 0
}

# ---------------------------------------------------------------------------
# Update Menu
# ---------------------------------------------------------------------------
show_update_menu() {
    show_banner
    echo -e "  ${BOLD}Update Components${NC}"
    echo ""

    if [ "$CS_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[1]${NC}  CrowdSec Engine  ${DIM}(apt/package manager)${NC}"
    fi
    if [ "$PARSER_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[2]${NC}  UniFi Parser Collection  ${DIM}(cscli upgrade)${NC}"
    fi
    if [ "$BOUNCER_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[3]${NC}  UniFi Bouncer  ${DIM}(re-run bootstrap)${NC}"
    fi
    if [ "$SIDECAR_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[4]${NC}  Sidecar Proxy  ${DIM}(docker pull)${NC}"
    fi
    if [ "$BLOCKLIST_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[5]${NC}  Blocklist Import  ${DIM}(docker pull)${NC}"
    fi
    echo ""
    echo -e "  ${BOLD}[A]${NC}  Update All"
    print_line
    echo -e "  ${BOLD}[B]${NC}  Back to main menu"
    echo ""

    read -n 1 -s choice </dev/tty
    case "$choice" in
        1) update_crowdsec; press_any_key ;;
        2) update_parser; press_any_key ;;
        3) update_bouncer; press_any_key ;;
        4) update_sidecar; press_any_key ;;
        5) update_blocklist; press_any_key ;;
        a|A) update_all; press_any_key ;;
        b|B) return ;;
        *) show_update_menu ;;
    esac
}

update_crowdsec() {
    msg_info "Updating CrowdSec Engine..."
    if command -v apt-get >/dev/null 2>&1; then
        apt-get update -qq && apt-get install --only-upgrade -y crowdsec 2>&1 | tee -a "$LOG_FILE"
    elif command -v yum >/dev/null 2>&1; then
        yum update -y crowdsec 2>&1 | tee -a "$LOG_FILE"
    else
        msg_warn "Unknown package manager. Update CrowdSec manually."
        return 1
    fi
    cscli hub update 2>&1 | tee -a "$LOG_FILE" || true
    msg_ok "CrowdSec Engine updated."
}

update_parser() {
    msg_info "Updating UniFi Parser Collection..."
    if cscli collections upgrade wolffcatskyy/unifi 2>&1 | tee -a "$LOG_FILE"; then
        systemctl reload crowdsec 2>/dev/null || true
        msg_ok "Parser collection updated."
    else
        msg_warn "Update failed. Trying fresh install..."
        cscli collections install wolffcatskyy/unifi --force 2>&1 | tee -a "$LOG_FILE" || true
    fi
}

update_bouncer() {
    msg_info "Updating UniFi Bouncer..."
    msg_info "Re-running bootstrap installer..."
    if curl -sSL "${GITHUB_RAW}/crowdsec-unifi-bouncer/main/bootstrap.sh" | bash 2>&1 | tee -a "$LOG_FILE"; then
        systemctl restart crowdsec-firewall-bouncer 2>/dev/null || true
        msg_ok "Bouncer updated."
    else
        msg_err "Bouncer update failed."
        return 1
    fi
}

update_sidecar() {
    msg_info "Updating Sidecar Proxy..."
    local install_dir="/opt/crowdsec-suite"
    if [ -f "${install_dir}/docker-compose.yml" ]; then
        cd "$install_dir"
        docker compose pull crowdsec-sidecar 2>&1 | tee -a "$LOG_FILE"
        docker compose up -d crowdsec-sidecar 2>&1 | tee -a "$LOG_FILE"
        msg_ok "Sidecar Proxy updated."
    else
        msg_err "Compose file not found at ${install_dir}/docker-compose.yml"
        return 1
    fi
}

update_blocklist() {
    msg_info "Updating Blocklist Import..."
    local install_dir="/opt/crowdsec-suite"
    if [ -f "${install_dir}/docker-compose.yml" ]; then
        cd "$install_dir"
        docker compose pull blocklist-import 2>&1 | tee -a "$LOG_FILE"
        docker compose up -d blocklist-import 2>&1 | tee -a "$LOG_FILE"
        msg_ok "Blocklist Import updated."
    else
        msg_err "Compose file not found at ${install_dir}/docker-compose.yml"
        return 1
    fi
}

update_all() {
    show_banner
    echo -e "  ${BOLD}Updating All Components${NC}"
    echo ""
    [ "$CS_INSTALLED" = true ] && update_crowdsec
    echo ""
    [ "$PARSER_INSTALLED" = true ] && update_parser
    echo ""
    [ "$BOUNCER_INSTALLED" = true ] && update_bouncer
    echo ""
    [ "$SIDECAR_INSTALLED" = true ] && update_sidecar
    echo ""
    [ "$BLOCKLIST_INSTALLED" = true ] && update_blocklist
    echo ""
    msg_ok "All components updated."
}

# ---------------------------------------------------------------------------
# Uninstall Menu
# ---------------------------------------------------------------------------
show_uninstall_menu() {
    show_banner
    echo -e "  ${BOLD}Uninstall Components${NC}"
    echo ""
    echo -e "  ${RED}These actions are destructive and cannot be undone.${NC}"
    echo ""

    if [ "$BLOCKLIST_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[5]${NC}  Blocklist Import"
    fi
    if [ "$SIDECAR_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[4]${NC}  Sidecar Proxy"
    fi
    if [ "$BOUNCER_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[3]${NC}  UniFi Bouncer"
    fi
    if [ "$PARSER_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[2]${NC}  UniFi Parser Collection"
    fi
    if [ "$CS_INSTALLED" = true ]; then
        echo -e "  ${BOLD}[1]${NC}  CrowdSec Engine"
    fi
    echo ""
    echo -e "  ${BOLD}[A]${NC}  ${RED}Uninstall Everything${NC}"
    print_line
    echo -e "  ${BOLD}[B]${NC}  Back to main menu"
    echo ""

    read -n 1 -s choice </dev/tty
    case "$choice" in
        1) confirm_and_run uninstall_crowdsec "CrowdSec Engine" ;;
        2) confirm_and_run uninstall_parser "UniFi Parser Collection" ;;
        3) confirm_and_run uninstall_bouncer "UniFi Bouncer" ;;
        4) confirm_and_run uninstall_sidecar "Sidecar Proxy" ;;
        5) confirm_and_run uninstall_blocklist "Blocklist Import" ;;
        a|A) confirm_and_run uninstall_all "ALL components" ;;
        b|B) return ;;
        *) show_uninstall_menu ;;
    esac
}

confirm_and_run() {
    local func="$1"
    local name="$2"

    echo ""
    echo -e "  ${RED}${BOLD}Uninstall ${name}? This cannot be undone.${NC}"
    echo -e "  ${BOLD}Type 'yes' to confirm:${NC} "
    read -r confirm </dev/tty
    if [ "$confirm" = "yes" ]; then
        "$func"
        detect_environment
        press_any_key
    else
        msg_info "Cancelled."
        press_any_key
    fi
}

uninstall_crowdsec() {
    msg_info "Uninstalling CrowdSec Engine..."
    log "Uninstalling CrowdSec Engine"

    # Stop service first
    systemctl stop crowdsec 2>/dev/null || true
    systemctl disable crowdsec 2>/dev/null || true

    if command -v apt-get >/dev/null 2>&1; then
        apt-get remove -y crowdsec 2>&1 | tee -a "$LOG_FILE" || true
    elif command -v yum >/dev/null 2>&1; then
        yum remove -y crowdsec 2>&1 | tee -a "$LOG_FILE" || true
    elif command -v apk >/dev/null 2>&1; then
        apk del crowdsec 2>&1 | tee -a "$LOG_FILE" || true
    else
        msg_warn "Unknown package manager. Remove CrowdSec packages manually."
    fi

    CS_INSTALLED=false
    PARSER_INSTALLED=false
    msg_ok "CrowdSec Engine uninstalled."
}

uninstall_parser() {
    msg_info "Uninstalling UniFi Parser Collection..."
    log "Uninstalling parser collection"

    if command -v cscli >/dev/null 2>&1; then
        cscli collections remove wolffcatskyy/unifi --force 2>&1 | tee -a "$LOG_FILE" || true
        systemctl reload crowdsec 2>/dev/null || true
    fi

    PARSER_INSTALLED=false
    msg_ok "UniFi Parser Collection removed."
}

uninstall_bouncer() {
    msg_info "Uninstalling UniFi Bouncer..."
    log "Uninstalling bouncer"

    # Stop service
    systemctl stop crowdsec-firewall-bouncer 2>/dev/null || true
    systemctl disable crowdsec-firewall-bouncer 2>/dev/null || true

    # Remove systemd units
    rm -f /etc/systemd/system/crowdsec-firewall-bouncer.service 2>/dev/null
    rm -f /etc/systemd/system/crowdsec-unifi-metrics.service 2>/dev/null
    systemctl daemon-reload 2>/dev/null || true

    # Clean ipsets
    msg_info "Cleaning up ipsets..."
    ipset destroy crowdsec-blacklists 2>/dev/null || true
    ipset destroy crowdsec6-blacklists 2>/dev/null || true

    # Clean iptables rules
    iptables -D INPUT -m set --match-set crowdsec-blacklists src -j DROP 2>/dev/null || true
    ip6tables -D INPUT -m set --match-set crowdsec6-blacklists src -j DROP 2>/dev/null || true
    iptables -D FORWARD -m set --match-set crowdsec-blacklists src -j DROP 2>/dev/null || true
    ip6tables -D FORWARD -m set --match-set crowdsec6-blacklists src -j DROP 2>/dev/null || true

    # Remove bouncer directory
    rm -rf /data/crowdsec-bouncer 2>/dev/null

    BOUNCER_INSTALLED=false
    msg_ok "UniFi Bouncer uninstalled and ipsets cleaned."
}

uninstall_sidecar() {
    msg_info "Uninstalling Sidecar Proxy..."
    log "Uninstalling sidecar"

    local install_dir="/opt/crowdsec-suite"
    if [ -f "${install_dir}/docker-compose.yml" ]; then
        cd "$install_dir"
        docker compose down crowdsec-sidecar 2>&1 | tee -a "$LOG_FILE" || true
    else
        docker stop crowdsec-sidecar 2>/dev/null || true
        docker rm crowdsec-sidecar 2>/dev/null || true
    fi

    SIDECAR_INSTALLED=false
    ABUSEIPDB_ENABLED=false
    msg_ok "Sidecar Proxy removed."
}

uninstall_blocklist() {
    msg_info "Uninstalling Blocklist Import..."
    log "Uninstalling blocklist import"

    local install_dir="/opt/crowdsec-suite"
    if [ -f "${install_dir}/docker-compose.yml" ]; then
        cd "$install_dir"
        docker compose down blocklist-import 2>&1 | tee -a "$LOG_FILE" || true
    else
        docker stop crowdsec-blocklist-import 2>/dev/null || true
        docker rm crowdsec-blocklist-import 2>/dev/null || true
    fi

    BLOCKLIST_INSTALLED=false
    msg_ok "Blocklist Import removed."
}

uninstall_all() {
    msg_info "Uninstalling all CrowdSec UniFi Suite components..."
    log "Uninstalling all components"

    # Reverse order: dependents first
    [ "$BLOCKLIST_INSTALLED" = true ] && uninstall_blocklist
    [ "$SIDECAR_INSTALLED" = true ] && uninstall_sidecar
    [ "$BOUNCER_INSTALLED" = true ] && uninstall_bouncer
    [ "$PARSER_INSTALLED" = true ] && uninstall_parser
    [ "$CS_INSTALLED" = true ] && uninstall_crowdsec

    # Clean up suite directory
    if [ -d "/opt/crowdsec-suite" ]; then
        echo ""
        echo -e "  ${BOLD}Remove /opt/crowdsec-suite directory? [y/N]${NC} "
        read -n 1 -s rm_dir </dev/tty
        echo ""
        if [ "$rm_dir" = "y" ] || [ "$rm_dir" = "Y" ]; then
            rm -rf /opt/crowdsec-suite
            msg_ok "Removed /opt/crowdsec-suite"
        fi
    fi

    # Remove log file
    rm -f "$LOG_FILE" 2>/dev/null

    echo ""
    msg_ok "All CrowdSec UniFi Suite components have been removed."
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
    log_init
    check_root
    detect_environment

    while true; do
        show_banner
        show_main_menu
    done
}

main "$@"