#!/bin/bash
################################################################################
# Script Name: install-unbound.sh
# Version: 1.1
# Description: Install and configure Unbound recursive DNS resolver with
#              DNSSEC validation, DNS-over-TLS forwarding, local zones,
#              unbound-control, and optional NSD stub-zone pairing
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
# Date: 2026-03-31
#
# Supported OS:
#   - Ubuntu / Debian
#   - RHEL / AlmaLinux / Rocky Linux / Fedora
#
# Usage:
#   sudo ./install-unbound.sh
#   sudo ./install-unbound.sh --forward cloudflare --dot
#   sudo ./install-unbound.sh --forward google --local-zone home.lab
#   sudo ./install-unbound.sh --nsd-stub example.com --nsd-port 5353
#   sudo ./install-unbound.sh --uninstall
#   ./install-unbound.sh --dry-run
#
################################################################################

set -euo pipefail

# ============================================================================
# CONFIGURATION
# ============================================================================

readonly VERSION="1.0"
readonly SCRIPT_NAME="${0##*/}"
readonly LOG_FILE="/var/log/unbound-install.log"

# Defaults
MODE="recursive"          # recursive (root hints) or forward
FORWARDER=""              # cloudflare, google, quad9, or custom IP
USE_DOT=false             # DNS-over-TLS for forwarding
LOCAL_ZONES=()            # Local zone names to create
LOCAL_ENTRIES=()          # Individual local-data entries
NSD_STUB_ZONES=()         # Zones to stub to NSD
NSD_PORT="5353"           # Port NSD listens on
LISTEN_IP="127.0.0.1"     # Default: localhost only
LISTEN_ALL=false          # Listen on all interfaces
THREADS=""                # Auto-detect from CPU cores
CACHE_SIZE=""             # Auto-detect from RAM
UNINSTALL=false
DRY_RUN=false

# Paths (set after OS detection)
UNBOUND_CONF=""
UNBOUND_CONF_DIR=""
UNBOUND_USER=""
TRUST_ANCHOR=""
ROOT_HINTS=""
TLS_BUNDLE=""

# OS detection
OS=""
OS_FAMILY=""

# ============================================================================
# COLOR OUTPUT
# ============================================================================

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

print_success() { echo -e "${GREEN}  ✓ $1${NC}"; }
print_error()   { echo -e "${RED}  ✗ $1${NC}" >&2; }
print_warning() { echo -e "${YELLOW}  ⚠ $1${NC}"; }
print_info()    { echo -e "  → $1"; }
print_header()  { echo -e "\n${CYAN}━━━ $1 ━━━${NC}"; }

# ============================================================================
# LOGGING
# ============================================================================

log() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
    if [[ "$DRY_RUN" == "false" ]] && [[ -w "$(dirname "$LOG_FILE")" || -w "$LOG_FILE" ]]; then
        echo "$msg" >> "$LOG_FILE"
    fi
    echo "$msg"
}

log_error() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1"
    if [[ "$DRY_RUN" == "false" ]] && [[ -w "$(dirname "$LOG_FILE")" || -w "$LOG_FILE" ]]; then
        echo "$msg" >> "$LOG_FILE"
    fi
    print_error "$1"
}

# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

show_help() {
    cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]

Install and configure Unbound recursive DNS resolver.

MODE OPTIONS:
    --recursive             Full recursion from root hints (default)
    --forward PROVIDER      Forward to upstream: cloudflare, google, quad9, or IP
    --dot                   Use DNS-over-TLS for forwarding (requires --forward)

NETWORK:
    --listen IP             IP to listen on (default: 127.0.0.1)
    --listen-all            Listen on all interfaces (0.0.0.0)
    --allow-net CIDR        Allow queries from network (repeatable, default: 127.0.0.0/8)

LOCAL ZONES:
    --local-zone NAME       Create a local zone (repeatable)
    --local-data "RECORD"   Add a local-data entry (repeatable)
                            Format: "hostname.zone. A 10.0.1.10"

NSD INTEGRATION:
    --nsd-stub ZONE         Add stub-zone for NSD (repeatable)
    --nsd-port PORT         NSD port (default: 5353)

TUNING:
    --threads N             Number of threads (default: auto-detect CPU cores)
    --cache-size SIZE       Message cache size in MB (default: auto-detect)

ACTIONS:
    --uninstall             Remove Unbound and configuration
    --dry-run               Show what would be done without executing
    -h, --help              Show this help message
    --version               Show version

EXAMPLES:
    $SCRIPT_NAME
    $SCRIPT_NAME --forward cloudflare --dot
    $SCRIPT_NAME --forward google --listen-all --allow-net 10.0.0.0/8
    $SCRIPT_NAME --local-zone home.lab --local-data "nas.home.lab. A 192.168.1.100"
    $SCRIPT_NAME --nsd-stub example.com --nsd-port 5353
    $SCRIPT_NAME --uninstall

EOF
    exit 0
}

show_version() {
    echo "$SCRIPT_NAME version $VERSION"
    exit 0
}

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

# ============================================================================
# OS DETECTION
# ============================================================================

detect_os() {
    if [[ ! -f /etc/os-release ]]; then
        log_error "Cannot detect OS — /etc/os-release not found"
        exit 1
    fi

    OS=$({ grep ^ID= /etc/os-release || true; } | cut -d= -f2 | tr -d '"' | tr '[:upper:]' '[:lower:]')

    case "$OS" in
        ubuntu|debian)
            OS_FAMILY="debian"
            UNBOUND_CONF="/etc/unbound/unbound.conf"
            UNBOUND_CONF_DIR="/etc/unbound/unbound.conf.d"
            UNBOUND_USER="unbound"
            TRUST_ANCHOR="/var/lib/unbound/root.key"
            ROOT_HINTS="/usr/share/dns/root.hints"
            TLS_BUNDLE="/etc/ssl/certs/ca-certificates.crt"
            ;;
        rhel|centos|rocky|almalinux|fedora)
            OS_FAMILY="rhel"
            UNBOUND_CONF="/etc/unbound/unbound.conf"
            UNBOUND_CONF_DIR="/etc/unbound/conf.d"
            UNBOUND_USER="unbound"
            TRUST_ANCHOR="/var/lib/unbound/root.key"
            ROOT_HINTS="/etc/unbound/root.hints"
            TLS_BUNDLE="/etc/pki/tls/certs/ca-bundle.crt"
            ;;
        *)
            log_error "Unsupported OS: $OS"
            echo "Supported: Ubuntu, Debian, RHEL, AlmaLinux, Rocky Linux, Fedora"
            exit 1
            ;;
    esac

    log "Detected OS: $OS (family: $OS_FAMILY)"
}

# ============================================================================
# SYSTEMD-RESOLVED HANDLING
# ============================================================================

disable_resolved() {
    if [[ "$OS" != "ubuntu" ]]; then
        return
    fi

    if systemctl is-active --quiet systemd-resolved 2>/dev/null; then
        print_warning "systemd-resolved is running and binds to port 53"
        if [[ "$DRY_RUN" == "true" ]]; then
            print_info "[DRY RUN] Would disable systemd-resolved"
            return
        fi

        log "Disabling systemd-resolved to free port 53"
        systemctl stop systemd-resolved
        systemctl disable systemd-resolved

        if [[ -L /etc/resolv.conf ]]; then
            rm -f /etc/resolv.conf
            echo "nameserver 1.1.1.1" > /etc/resolv.conf
        fi

        print_success "Disabled systemd-resolved"
    else
        print_info "systemd-resolved not running — no action needed"
    fi
}

# ============================================================================
# INSTALL UNBOUND
# ============================================================================

install_unbound() {
    print_header "Installing Unbound"

    if command -v unbound &>/dev/null; then
        print_info "Unbound is already installed"
        return
    fi

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would install unbound packages"
        return
    fi

    log "Installing Unbound packages"

    case "$OS_FAMILY" in
        debian)
            apt-get update -qq
            apt-get install -y -qq unbound unbound-host dns-root-data > /dev/null
            ;;
        rhel)
            if [[ "$OS" != "fedora" ]]; then
                dnf install -y -q epel-release > /dev/null 2>&1 || true
            fi
            dnf install -y -q unbound > /dev/null
            ;;
    esac

    print_success "Unbound installed"
}

# ============================================================================
# AUTO-DETECT TUNING
# ============================================================================

detect_tuning() {
    if [[ -z "$THREADS" ]]; then
        THREADS=$(nproc 2>/dev/null || echo 2)
    fi

    if [[ -z "$CACHE_SIZE" ]]; then
        local total_mb
        total_mb=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 1024)
        if (( total_mb <= 1024 )); then
            CACHE_SIZE="16"
        elif (( total_mb <= 2048 )); then
            CACHE_SIZE="32"
        elif (( total_mb <= 4096 )); then
            CACHE_SIZE="64"
        else
            CACHE_SIZE="128"
        fi
    fi

    # Slabs must be power of 2 >= threads
    local slabs=2
    while (( slabs < THREADS )); do
        slabs=$((slabs * 2))
    done
    SLAB_COUNT=$slabs

    local rrset_size=$((CACHE_SIZE * 2))

    log "Tuning: threads=$THREADS, msg-cache=${CACHE_SIZE}m, rrset-cache=${rrset_size}m, slabs=$SLAB_COUNT"
}

# ============================================================================
# SETUP ROOT HINTS
# ============================================================================

setup_root_hints() {
    if [[ "$MODE" != "recursive" ]]; then
        return
    fi

    print_info "Setting up root hints"

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would download root hints"
        return
    fi

    local hints_dir
    hints_dir=$(dirname "$ROOT_HINTS")
    mkdir -p "$hints_dir"

    if wget -qO "$ROOT_HINTS.tmp" https://www.internic.net/domain/named.cache 2>/dev/null; then
        mv "$ROOT_HINTS.tmp" "$ROOT_HINTS"
        chown "$UNBOUND_USER":"$UNBOUND_USER" "$ROOT_HINTS" 2>/dev/null || true
        print_success "Root hints downloaded"
    else
        print_warning "Could not download root hints — using package default"
    fi
}

# ============================================================================
# SETUP TRUST ANCHOR
# ============================================================================

setup_trust_anchor() {
    print_info "Setting up DNSSEC trust anchor"

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would set up DNSSEC trust anchor"
        return
    fi

    local anchor_dir
    anchor_dir=$(dirname "$TRUST_ANCHOR")
    mkdir -p "$anchor_dir"
    chown "$UNBOUND_USER":"$UNBOUND_USER" "$anchor_dir"

    if command -v unbound-anchor &>/dev/null; then
        unbound-anchor -a "$TRUST_ANCHOR" 2>/dev/null || true
        chown "$UNBOUND_USER":"$UNBOUND_USER" "$TRUST_ANCHOR" 2>/dev/null || true
        print_success "DNSSEC trust anchor configured"
    else
        print_warning "unbound-anchor not found — DNSSEC trust anchor not configured"
    fi
}

# ============================================================================
# SETUP UNBOUND-CONTROL
# ============================================================================

setup_control() {
    print_info "Setting up unbound-control"

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would generate unbound-control keys"
        return
    fi

    if [[ -f /etc/unbound/unbound_control.key ]]; then
        print_info "unbound-control keys already exist"
        return
    fi

    if command -v unbound-control-setup &>/dev/null; then
        unbound-control-setup > /dev/null 2>&1
        print_success "unbound-control keys generated"
    else
        print_warning "unbound-control-setup not found"
    fi
}

# ============================================================================
# GENERATE CONFIGURATION
# ============================================================================

generate_config() {
    print_header "Generating Configuration"

    local rrset_size=$((CACHE_SIZE * 2))
    local listen_addr="$LISTEN_IP"
    if [[ "$LISTEN_ALL" == "true" ]]; then
        listen_addr="0.0.0.0"
    fi

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would write $UNBOUND_CONF"
        print_info "  Listen: $listen_addr"
        print_info "  Mode: $MODE"
        [[ -n "$FORWARDER" ]] && print_info "  Forwarder: $FORWARDER (DoT: $USE_DOT)"
        print_info "  Threads: $THREADS, Cache: ${CACHE_SIZE}m"
        [[ ${#LOCAL_ZONES[@]} -gt 0 ]] && print_info "  Local zones: ${LOCAL_ZONES[*]}"
        [[ ${#NSD_STUB_ZONES[@]} -gt 0 ]] && print_info "  NSD stub zones: ${NSD_STUB_ZONES[*]} (port $NSD_PORT)"
        return
    fi

    # Backup existing config
    if [[ -f "$UNBOUND_CONF" ]]; then
        cp "$UNBOUND_CONF" "${UNBOUND_CONF}.bak.$(date +%Y%m%d%H%M%S)"
        print_info "Backed up existing config"
    fi

    mkdir -p "$UNBOUND_CONF_DIR"

    # ── Main config ──
    cat > "$UNBOUND_CONF" <<CONF
# Unbound configuration — generated by $SCRIPT_NAME v$VERSION
# Date: $(date '+%Y-%m-%d %H:%M:%S')

server:
    # Network
    interface: $listen_addr
    port: 53
    do-ip4: yes
    do-ip6: no
    do-udp: yes
    do-tcp: yes

    # Access control
    access-control: 127.0.0.0/8 allow
CONF

    if [[ "$LISTEN_ALL" == "true" ]]; then
        cat >> "$UNBOUND_CONF" <<CONF
    access-control: 10.0.0.0/8 allow
    access-control: 172.16.0.0/12 allow
    access-control: 192.168.0.0/16 allow
CONF
    fi

    # Add any extra --allow-net entries
    for net in "${ALLOW_NETS[@]:-}"; do
        [[ -n "$net" ]] && echo "    access-control: $net allow" >> "$UNBOUND_CONF"
    done

    cat >> "$UNBOUND_CONF" <<CONF

    # DNSSEC
    auto-trust-anchor-file: "$TRUST_ANCHOR"

    # Security
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    harden-referral-path: yes
    use-caps-for-id: yes
    qname-minimisation: yes
    aggressive-nsec: yes

    # Cache
    cache-min-ttl: 300
    cache-max-ttl: 86400
    prefetch: yes
    prefetch-key: yes
    serve-expired: yes
    serve-expired-ttl: 86400

    # Performance
    num-threads: $THREADS
    msg-cache-slabs: $SLAB_COUNT
    rrset-cache-slabs: $SLAB_COUNT
    infra-cache-slabs: $SLAB_COUNT
    key-cache-slabs: $SLAB_COUNT
    msg-cache-size: ${CACHE_SIZE}m
    rrset-cache-size: ${rrset_size}m
    outgoing-range: 4096
    num-queries-per-thread: 2048
    so-reuseport: yes

    # Logging
    verbosity: 1
    log-queries: no
    log-replies: no
    logfile: ""

    # Private addresses
    private-address: 10.0.0.0/8
    private-address: 172.16.0.0/12
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: fd00::/8
    private-address: fe80::/10

    # Runtime
    username: "$UNBOUND_USER"
    directory: "/etc/unbound"
    chroot: ""
    pidfile: "/run/unbound/unbound.pid"

CONF

    # Root hints (recursive mode only)
    if [[ "$MODE" == "recursive" ]]; then
        echo "    root-hints: \"$ROOT_HINTS\"" >> "$UNBOUND_CONF"
        echo "" >> "$UNBOUND_CONF"
    fi

    # TLS bundle (for DoT)
    if [[ "$USE_DOT" == "true" ]]; then
        echo "    tls-cert-bundle: \"$TLS_BUNDLE\"" >> "$UNBOUND_CONF"
        echo "" >> "$UNBOUND_CONF"
    fi

    # Include directory
    echo "    include: \"$UNBOUND_CONF_DIR/*.conf\"" >> "$UNBOUND_CONF"
    echo "" >> "$UNBOUND_CONF"

    # Remote control
    cat >> "$UNBOUND_CONF" <<CONF
remote-control:
    control-enable: yes
    control-interface: 127.0.0.1
    control-port: 8953
CONF

    print_success "Main config written to $UNBOUND_CONF"

    # ── Forwarding config ──
    if [[ "$MODE" == "forward" && -n "$FORWARDER" ]]; then
        generate_forward_config
    fi

    # ── Local zones config ──
    if [[ ${#LOCAL_ZONES[@]} -gt 0 || ${#LOCAL_ENTRIES[@]} -gt 0 ]]; then
        generate_local_zones_config
    fi

    # ── NSD stub zones config ──
    if [[ ${#NSD_STUB_ZONES[@]} -gt 0 ]]; then
        generate_nsd_stub_config
    fi
}

# ============================================================================
# FORWARDING CONFIGURATION
# ============================================================================

generate_forward_config() {
    local conf_file="$UNBOUND_CONF_DIR/forwarding.conf"
    local dot_flag=""
    [[ "$USE_DOT" == "true" ]] && dot_flag="yes" || dot_flag="no"

    cat > "$conf_file" <<CONF
# Forwarding configuration — generated by $SCRIPT_NAME
CONF

    case "$FORWARDER" in
        cloudflare)
            if [[ "$USE_DOT" == "true" ]]; then
                cat >> "$conf_file" <<CONF
forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 1.1.1.1@853#cloudflare-dns.com
    forward-addr: 1.0.0.1@853#cloudflare-dns.com
CONF
            else
                cat >> "$conf_file" <<CONF
forward-zone:
    name: "."
    forward-addr: 1.1.1.1
    forward-addr: 1.0.0.1
CONF
            fi
            ;;
        google)
            if [[ "$USE_DOT" == "true" ]]; then
                cat >> "$conf_file" <<CONF
forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 8.8.8.8@853#dns.google
    forward-addr: 8.8.4.4@853#dns.google
CONF
            else
                cat >> "$conf_file" <<CONF
forward-zone:
    name: "."
    forward-addr: 8.8.8.8
    forward-addr: 8.8.4.4
CONF
            fi
            ;;
        quad9)
            if [[ "$USE_DOT" == "true" ]]; then
                cat >> "$conf_file" <<CONF
forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 9.9.9.9@853#dns.quad9.net
    forward-addr: 149.112.112.112@853#dns.quad9.net
CONF
            else
                cat >> "$conf_file" <<CONF
forward-zone:
    name: "."
    forward-addr: 9.9.9.9
    forward-addr: 149.112.112.112
CONF
            fi
            ;;
        *)
            # Custom IP address
            cat >> "$conf_file" <<CONF
forward-zone:
    name: "."
    forward-tls-upstream: $dot_flag
    forward-addr: $FORWARDER
CONF
            ;;
    esac

    print_success "Forwarding config written (upstream: $FORWARDER, DoT: $USE_DOT)"
}

# ============================================================================
# LOCAL ZONES CONFIGURATION
# ============================================================================

generate_local_zones_config() {
    local conf_file="$UNBOUND_CONF_DIR/local-zones.conf"

    cat > "$conf_file" <<CONF
# Local zones — generated by $SCRIPT_NAME
server:
CONF

    for zone in "${LOCAL_ZONES[@]}"; do
        echo "    local-zone: \"${zone}.\" static" >> "$conf_file"
    done

    for entry in "${LOCAL_ENTRIES[@]}"; do
        echo "    local-data: \"$entry\"" >> "$conf_file"
    done

    print_success "Local zones config written (${#LOCAL_ZONES[@]} zones, ${#LOCAL_ENTRIES[@]} records)"
}

# ============================================================================
# NSD STUB ZONE CONFIGURATION
# ============================================================================

generate_nsd_stub_config() {
    local conf_file="$UNBOUND_CONF_DIR/nsd-stubs.conf"

    cat > "$conf_file" <<CONF
# NSD stub zones — generated by $SCRIPT_NAME
# Forwards these zones to local NSD on port $NSD_PORT
CONF

    for zone in "${NSD_STUB_ZONES[@]}"; do
        cat >> "$conf_file" <<CONF

stub-zone:
    name: "$zone"
    stub-addr: 127.0.0.1@$NSD_PORT
CONF
    done

    print_success "NSD stub zones written (${#NSD_STUB_ZONES[@]} zones → port $NSD_PORT)"
}

# ============================================================================
# FIREWALL CONFIGURATION
# ============================================================================

configure_firewall() {
    if [[ "$LISTEN_ALL" != "true" && "$LISTEN_IP" == "127.0.0.1" ]]; then
        print_info "Listening on localhost only — skipping firewall"
        return
    fi

    print_info "Configuring firewall"

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would open port 53/tcp and 53/udp"
        return
    fi

    if command -v ufw &>/dev/null && ufw status | grep -q "active"; then
        ufw allow 53/tcp > /dev/null 2>&1
        ufw allow 53/udp > /dev/null 2>&1
        print_success "Opened ports 53/tcp and 53/udp (ufw)"
    elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then
        firewall-cmd --permanent --add-service=dns > /dev/null 2>&1
        firewall-cmd --reload > /dev/null 2>&1
        print_success "Opened DNS ports (firewalld)"
    else
        print_warning "No active firewall detected — skipping"
    fi
}

# ============================================================================
# START AND VALIDATE
# ============================================================================

validate_and_start() {
    print_header "Validating and Starting Unbound"

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would validate config and start unbound"
        return
    fi

    # Create pid directory
    mkdir -p /run/unbound
    chown "$UNBOUND_USER":"$UNBOUND_USER" /run/unbound

    # Set ownership
    chown -R "$UNBOUND_USER":"$UNBOUND_USER" /etc/unbound/ 2>/dev/null || true
    chmod 640 "$UNBOUND_CONF"

    # Validate
    if unbound-checkconf "$UNBOUND_CONF" > /dev/null 2>&1; then
        print_success "Configuration validated (unbound-checkconf)"
    else
        print_error "Configuration validation failed:"
        unbound-checkconf "$UNBOUND_CONF"
        exit 1
    fi

    # Enable and start
    systemctl enable unbound > /dev/null 2>&1
    systemctl restart unbound

    if systemctl is-active --quiet unbound; then
        print_success "Unbound is running"
    else
        print_error "Unbound failed to start"
        journalctl -u unbound --no-pager -n 10
        exit 1
    fi

    # Quick test
    sleep 1
    if dig @127.0.0.1 example.com A +short +time=5 > /dev/null 2>&1; then
        print_success "DNS resolution test passed"
    else
        print_warning "DNS resolution test failed — check logs with: journalctl -u unbound"
    fi
}

# ============================================================================
# UNINSTALL
# ============================================================================

do_uninstall() {
    print_header "Uninstalling Unbound"

    if [[ "$DRY_RUN" == "true" ]]; then
        print_info "[DRY RUN] Would remove Unbound and configuration"
        return
    fi

    systemctl stop unbound 2>/dev/null || true
    systemctl disable unbound 2>/dev/null || true

    case "$OS_FAMILY" in
        debian)
            apt-get remove --purge -y -qq unbound unbound-host dns-root-data > /dev/null 2>&1 || true
            apt-get autoremove -y -qq > /dev/null 2>&1 || true
            ;;
        rhel)
            dnf remove -y -q unbound > /dev/null 2>&1 || true
            ;;
    esac

    rm -rf /etc/unbound
    rm -f /var/lib/unbound/root.key

    print_success "Unbound removed"
    log "Unbound uninstalled"
}

# ============================================================================
# SUMMARY
# ============================================================================

print_summary() {
    local rrset_size=$((CACHE_SIZE * 2))

    print_header "Installation Complete"

    echo ""
    echo "  Unbound recursive DNS resolver is running."
    echo ""
    echo "  Config:        $UNBOUND_CONF"
    echo "  Config dir:    $UNBOUND_CONF_DIR"
    echo "  Listen:        ${LISTEN_ALL:+0.0.0.0}${LISTEN_ALL:-$LISTEN_IP}:53"
    echo "  Mode:          $MODE"
    [[ -n "$FORWARDER" ]] && echo "  Forwarder:     $FORWARDER (DoT: $USE_DOT)"
    echo "  Threads:       $THREADS"
    echo "  Cache:         msg=${CACHE_SIZE}m, rrset=${rrset_size}m"
    echo "  DNSSEC:        enabled"
    [[ ${#LOCAL_ZONES[@]} -gt 0 ]] && echo "  Local zones:   ${LOCAL_ZONES[*]}"
    [[ ${#NSD_STUB_ZONES[@]} -gt 0 ]] && echo "  NSD stubs:     ${NSD_STUB_ZONES[*]} (port $NSD_PORT)"
    echo "  Log:           $LOG_FILE"
    echo ""
    echo "  Useful commands:"
    echo "    sudo unbound-control status"
    echo "    sudo unbound-control stats_noreset"
    echo "    dig @127.0.0.1 example.com A"
    echo "    sudo journalctl -u unbound -f"
    echo ""
}

# ============================================================================
# PARSE ARGUMENTS
# ============================================================================

ALLOW_NETS=()

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --recursive)
                MODE="recursive"
                shift
                ;;
            --forward)
                MODE="forward"
                FORWARDER="${2:?'--forward requires a provider (cloudflare, google, quad9, or IP)'}"
                shift 2
                ;;
            --dot)
                USE_DOT=true
                shift
                ;;
            --listen)
                LISTEN_IP="${2:?'--listen requires an IP address'}"
                shift 2
                ;;
            --listen-all)
                LISTEN_ALL=true
                shift
                ;;
            --allow-net)
                ALLOW_NETS+=("${2:?'--allow-net requires a CIDR'}")
                shift 2
                ;;
            --local-zone)
                LOCAL_ZONES+=("${2:?'--local-zone requires a zone name'}")
                shift 2
                ;;
            --local-data)
                LOCAL_ENTRIES+=("${2:?'--local-data requires a record'}")
                shift 2
                ;;
            --nsd-stub)
                NSD_STUB_ZONES+=("${2:?'--nsd-stub requires a zone name'}")
                shift 2
                ;;
            --nsd-port)
                NSD_PORT="${2:?'--nsd-port requires a port number'}"
                shift 2
                ;;
            --threads)
                THREADS="${2:?'--threads requires a number'}"
                shift 2
                ;;
            --cache-size)
                CACHE_SIZE="${2:?'--cache-size requires a size in MB'}"
                shift 2
                ;;
            --uninstall)
                UNINSTALL=true
                shift
                ;;
            --dry-run)
                DRY_RUN=true
                shift
                ;;
            -h|--help)
                show_help
                ;;
            --version)
                show_version
                ;;
            *)
                log_error "Unknown option: $1"
                echo "Use --help for usage information"
                exit 1
                ;;
        esac
    done

    # Validate
    if [[ "$USE_DOT" == "true" && "$MODE" != "forward" ]]; then
        log_error "--dot requires --forward (DoT is for forwarding to upstream)"
        exit 1
    fi
}

# ============================================================================
# MAIN
# ============================================================================

main() {
    parse_args "$@"

    echo ""
    echo "  ╔══════════════════════════════════════════╗"
    echo "  ║       Unbound Installer v$VERSION        ║"
    echo "  ║       https://mylinux.work               ║"
    echo "  ╚══════════════════════════════════════════╝"
    echo ""

    [[ "$DRY_RUN" == "true" ]] && print_warning "DRY RUN MODE — no changes will be made"

    # Root check (skip for dry-run)
    if [[ "$DRY_RUN" == "false" ]]; then
        check_root
    fi

    detect_os

    if [[ "$UNINSTALL" == "true" ]]; then
        do_uninstall
        exit 0
    fi

    detect_tuning
    disable_resolved
    install_unbound
    setup_trust_anchor
    setup_root_hints
    setup_control
    generate_config
    configure_firewall
    validate_and_start
    print_summary

    log "Installation complete"
}

main "$@"
