#!/bin/sh
#
# SecuBox Service Exposure Manager
# Unified tool for port management, Tor hidden services, and HAProxy SSL
#

. /lib/functions.sh
. /usr/share/libubox/jshn.sh

CONFIG_NAME="secubox-exposure"
HAPROXY_CONFIG=""
HAPROXY_CERTS=""
TOR_HIDDEN_DIR=""
TOR_CONFIG=""

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

log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_err() { echo -e "${RED}[ERROR]${NC} $1"; }

load_config() {
    config_load "$CONFIG_NAME"
    config_get HAPROXY_CONFIG main haproxy_config "/srv/lxc/haproxy/rootfs/etc/haproxy/haproxy.cfg"
    config_get HAPROXY_CERTS main haproxy_certs "/srv/lxc/haproxy/rootfs/etc/haproxy/certs"
    config_get TOR_HIDDEN_DIR main tor_hidden_dir "/var/lib/tor/hidden_services"
    config_get TOR_CONFIG main tor_config "/etc/tor/torrc"
    config_get APP_PORT_START ranges app_start "8100"
    config_get APP_PORT_END ranges app_end "8199"
}

# ============================================================================
# PORT SCANNING & CONFLICT DETECTION
# ============================================================================

get_listening_ports() {
    # Returns: port address process
    netstat -tlnp 2>/dev/null | grep LISTEN | awk '{
        split($4, a, ":")
        port = a[length(a)]
        if (!seen[port]++) {
            split($7, p, "/")
            proc = p[2]
            if (proc == "") proc = "unknown"
            print port, $4, proc
        }
    }' | sort -n
}

cmd_scan() {
    log_info "Scanning listening services..."
    echo ""
    printf "%-6s %-20s %-15s %-10s\n" "PORT" "ADDRESS" "PROCESS" "STATUS"
    printf "%-6s %-20s %-15s %-10s\n" "------" "--------------------" "---------------" "----------"

    get_listening_ports | while read port addr proc; do
        # Determine if external
        case "$addr" in
            *0.0.0.0*|*::*) status="${GREEN}external${NC}" ;;
            *127.0.0.1*|*::1*) status="${YELLOW}local${NC}" ;;
            *) status="${CYAN}bound${NC}" ;;
        esac
        printf "%-6s %-20s %-15s " "$port" "$addr" "$proc"
        echo -e "$status"
    done
    echo ""
}

cmd_conflicts() {
    log_info "Checking for port conflicts..."
    echo ""

    local conflicts=0
    local TMP_PORTS="/tmp/ports_$$"

    # Get all configured ports from UCI
    > "$TMP_PORTS"

    # Check known services
    check_known_service() {
        local section="$1"
        local default_port config_path
        config_get default_port "$section" default_port
        config_get config_path "$section" config_path

        if [ -n "$config_path" ]; then
            # Extract UCI config and option
            local uci_config=$(echo "$config_path" | cut -d'.' -f1)
            local uci_option=$(echo "$config_path" | cut -d'.' -f2-)
            local actual_port=$(uci -q get "$config_path" 2>/dev/null)
            [ -z "$actual_port" ] && actual_port="$default_port"
            echo "$actual_port $section" >> "$TMP_PORTS"
        fi
    }
    config_foreach check_known_service known

    # Find duplicates
    sort "$TMP_PORTS" | uniq -d -w5 | while read port svc; do
        log_warn "Port $port is configured for multiple services!"
        grep "^$port " "$TMP_PORTS" | while read p s; do
            echo "  - $s"
        done
        conflicts=$((conflicts + 1))
    done

    # Check against actually listening ports
    get_listening_ports | while read port addr proc; do
        if grep -q "^$port " "$TMP_PORTS"; then
            local configured_svc=$(grep "^$port " "$TMP_PORTS" | head -1 | cut -d' ' -f2)
            # Check if process matches expected
            case "$configured_svc" in
                gitea) [ "$proc" != "gitea" ] && log_warn "Port $port: expected gitea, found $proc" ;;
                streamlit) echo "$proc" | grep -qv "python\|streamlit" && log_warn "Port $port: expected streamlit, found $proc" ;;
            esac
        fi
    done

    rm -f "$TMP_PORTS"

    if [ "$conflicts" -eq 0 ]; then
        log_ok "No port conflicts detected"
    fi
}

find_free_port() {
    local start="$1"
    local end="$2"
    local port="$start"

    while [ "$port" -le "$end" ]; do
        if ! netstat -tlnp 2>/dev/null | grep -q ":$port "; then
            echo "$port"
            return 0
        fi
        port=$((port + 1))
    done
    return 1
}

cmd_fix_port() {
    local service="$1"
    local new_port="$2"

    if [ -z "$service" ]; then
        log_err "Usage: secubox-exposure fix-port <service> [new_port]"
        return 1
    fi

    load_config

    # Get service config
    local config_path default_port
    config_get config_path "$service" config_path
    config_get default_port "$service" default_port

    if [ -z "$config_path" ]; then
        log_err "Unknown service: $service"
        return 1
    fi

    # Find free port if not specified
    if [ -z "$new_port" ]; then
        new_port=$(find_free_port "$APP_PORT_START" "$APP_PORT_END")
        if [ -z "$new_port" ]; then
            log_err "No free ports available in range $APP_PORT_START-$APP_PORT_END"
            return 1
        fi
    fi

    # Check if new port is free
    if netstat -tlnp 2>/dev/null | grep -q ":$new_port "; then
        log_err "Port $new_port is already in use"
        return 1
    fi

    log_info "Changing $service port to $new_port"

    # Update UCI
    if uci set "$config_path=$new_port" && uci commit; then
        log_ok "UCI config updated"

        # Restart service if it has an init script
        if [ -x "/etc/init.d/$service" ]; then
            log_info "Restarting $service..."
            /etc/init.d/"$service" restart
        fi

        log_ok "$service now listening on port $new_port"
    else
        log_err "Failed to update UCI config"
        return 1
    fi
}

# ============================================================================
# TOR HIDDEN SERVICES
# ============================================================================

cmd_tor_add() {
    local service="$1"
    local local_port="$2"
    local onion_port="${3:-80}"

    if [ -z "$service" ]; then
        log_err "Usage: secubox-exposure tor add <service> [local_port] [onion_port]"
        return 1
    fi

    load_config

    # Get local port from config if not specified
    if [ -z "$local_port" ]; then
        config_get local_port "$service" default_port
        if [ -z "$local_port" ]; then
            log_err "Cannot determine local port for $service"
            return 1
        fi
    fi

    local hidden_dir="$TOR_HIDDEN_DIR/$service"

    # Create hidden service directory
    mkdir -p "$hidden_dir"
    chmod 700 "$hidden_dir"
    chown tor:tor "$hidden_dir" 2>/dev/null || chown debian-tor:debian-tor "$hidden_dir" 2>/dev/null

    # Check if already configured in torrc
    if grep -q "HiddenServiceDir $hidden_dir" "$TOR_CONFIG" 2>/dev/null; then
        log_warn "Hidden service for $service already exists"
        local onion=$(cat "$hidden_dir/hostname" 2>/dev/null)
        [ -n "$onion" ] && log_info "Onion address: $onion"
        return 0
    fi

    # Add to torrc
    log_info "Adding hidden service for $service (127.0.0.1:$local_port -> :$onion_port)"

    cat >> "$TOR_CONFIG" << EOF

# Hidden service for $service (added by secubox-exposure)
HiddenServiceDir $hidden_dir
HiddenServicePort $onion_port 127.0.0.1:$local_port
EOF

    # Restart Tor
    log_info "Restarting Tor..."
    /etc/init.d/tor restart 2>/dev/null || systemctl restart tor 2>/dev/null

    # Wait for onion address
    log_info "Waiting for onion address generation..."
    local tries=0
    while [ ! -f "$hidden_dir/hostname" ] && [ "$tries" -lt 30 ]; do
        sleep 1
        tries=$((tries + 1))
    done

    if [ -f "$hidden_dir/hostname" ]; then
        local onion=$(cat "$hidden_dir/hostname")
        log_ok "Hidden service created!"
        echo ""
        echo -e "  ${CYAN}Service:${NC} $service"
        echo -e "  ${CYAN}Onion:${NC}   $onion"
        echo -e "  ${CYAN}Port:${NC}    $onion_port -> 127.0.0.1:$local_port"
        echo ""

        # Save to exposure UCI
        uci set "${CONFIG_NAME}.${service}=service"
        uci set "${CONFIG_NAME}.${service}.port=$local_port"
        uci set "${CONFIG_NAME}.${service}.tor=1"
        uci set "${CONFIG_NAME}.${service}.tor_onion=$onion"
        uci set "${CONFIG_NAME}.${service}.tor_port=$onion_port"
        uci commit "$CONFIG_NAME"

        # Sync to Tor Shield UCI
        local hs_name="hs_${service}"
        uci set "tor-shield.${hs_name}=hidden_service"
        uci set "tor-shield.${hs_name}.name=${service}"
        uci set "tor-shield.${hs_name}.enabled=1"
        uci set "tor-shield.${hs_name}.local_port=${local_port}"
        uci set "tor-shield.${hs_name}.onion_port=${onion_port}"
        uci set "tor-shield.${hs_name}.onion_address=${onion}"
        uci commit tor-shield
        log_ok "Synced to Tor Shield"
    else
        log_err "Failed to generate onion address"
        return 1
    fi
}

cmd_tor_list() {
    load_config

    log_info "Tor Hidden Services:"
    echo ""
    printf "%-15s %-62s %-10s\n" "SERVICE" "ONION ADDRESS" "PORT"
    printf "%-15s %-62s %-10s\n" "---------------" "--------------------------------------------------------------" "----------"

    # List from filesystem
    if [ -d "$TOR_HIDDEN_DIR" ]; then
        for dir in "$TOR_HIDDEN_DIR"/*/; do
            [ -d "$dir" ] || continue
            local svc=$(basename "$dir")
            local onion=""
            [ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname")

            # Get port from torrc
            local port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $2}')

            if [ -n "$onion" ]; then
                printf "%-15s %-62s %-10s\n" "$svc" "$onion" "${port:-80}"
            fi
        done
    fi
    echo ""
}

cmd_tor_remove() {
    local service="$1"

    if [ -z "$service" ]; then
        log_err "Usage: secubox-exposure tor remove <service>"
        return 1
    fi

    load_config

    local hidden_dir="$TOR_HIDDEN_DIR/$service"

    if [ ! -d "$hidden_dir" ]; then
        log_err "No hidden service found for $service"
        return 1
    fi

    log_info "Removing hidden service for $service"

    # Remove from torrc (remove the block)
    sed -i "/# Hidden service for $service/,/HiddenServicePort/d" "$TOR_CONFIG"

    # Remove directory
    rm -rf "$hidden_dir"

    # Update exposure UCI
    uci delete "${CONFIG_NAME}.${service}.tor" 2>/dev/null
    uci delete "${CONFIG_NAME}.${service}.tor_onion" 2>/dev/null
    uci delete "${CONFIG_NAME}.${service}.tor_port" 2>/dev/null
    uci commit "$CONFIG_NAME"

    # Remove from Tor Shield UCI
    local hs_name="hs_${service}"
    if uci -q get "tor-shield.${hs_name}" >/dev/null 2>&1; then
        uci delete "tor-shield.${hs_name}"
        uci commit tor-shield
        log_ok "Removed from Tor Shield"
    fi

    # Restart Tor
    /etc/init.d/tor restart 2>/dev/null || systemctl restart tor 2>/dev/null

    log_ok "Hidden service removed"
}

cmd_tor_sync() {
    load_config

    log_info "Syncing hidden services to Tor Shield..."
    local synced=0

    # List from filesystem and sync to Tor Shield
    if [ -d "$TOR_HIDDEN_DIR" ]; then
        for dir in "$TOR_HIDDEN_DIR"/*/; do
            [ -d "$dir" ] || continue
            local svc=$(basename "$dir")
            local onion=""
            [ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname")

            # Get port from torrc
            local port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $2}')
            local local_port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{split($3,a,":"); print a[2]}')

            if [ -n "$onion" ]; then
                local hs_name="hs_${svc}"
                if ! uci -q get "tor-shield.${hs_name}" >/dev/null 2>&1; then
                    log_info "Adding $svc to Tor Shield"
                    uci set "tor-shield.${hs_name}=hidden_service"
                    uci set "tor-shield.${hs_name}.name=${svc}"
                    uci set "tor-shield.${hs_name}.enabled=1"
                    uci set "tor-shield.${hs_name}.local_port=${local_port:-80}"
                    uci set "tor-shield.${hs_name}.onion_port=${port:-80}"
                    uci set "tor-shield.${hs_name}.onion_address=${onion}"
                    synced=$((synced + 1))
                fi
            fi
        done
    fi

    if [ "$synced" -gt 0 ]; then
        uci commit tor-shield
        log_ok "Synced $synced hidden service(s) to Tor Shield"
    else
        log_info "All hidden services already synced"
    fi
}

# ============================================================================
# HAPROXY SSL BACKENDS (UCI-based integration with haproxyctl)
# ============================================================================

# Sanitize name for UCI section (replace dots/hyphens with underscores)
sanitize_uci_name() {
    echo "$1" | sed 's/[.-]/_/g'
}

cmd_ssl_add() {
    local service="$1"
    local domain="$2"
    local local_port="$3"

    if [ -z "$service" ] || [ -z "$domain" ]; then
        log_err "Usage: secubox-exposure ssl add <service> <domain> [local_port]"
        return 1
    fi

    load_config

    # Get local port from config if not specified
    if [ -z "$local_port" ]; then
        config_get local_port "$service" default_port
        # Try to get from service UCI
        local config_path
        config_get config_path "$service" config_path
        if [ -n "$config_path" ]; then
            local configured_port=$(uci -q get "$config_path")
            [ -n "$configured_port" ] && local_port="$configured_port"
        fi
        if [ -z "$local_port" ]; then
            log_err "Cannot determine local port for $service. Specify it manually."
            return 1
        fi
    fi

    # Check if haproxyctl exists
    if [ ! -x "/usr/sbin/haproxyctl" ]; then
        log_err "haproxyctl not found. Is secubox-app-haproxy installed?"
        return 1
    fi

    # Sanitize names for UCI
    local backend_name="$service"
    local vhost_name=$(sanitize_uci_name "$domain")

    # Check if backend already exists in UCI
    if uci -q get "haproxy.${backend_name}" >/dev/null 2>&1; then
        log_warn "Backend '$backend_name' already exists in HAProxy UCI config"
    else
        # Create backend in HAProxy UCI config
        log_info "Adding backend '$backend_name' (127.0.0.1:$local_port)"
        uci set "haproxy.${backend_name}=backend"
        uci set "haproxy.${backend_name}.name=${backend_name}"
        uci set "haproxy.${backend_name}.mode=http"
        uci set "haproxy.${backend_name}.balance=roundrobin"
        uci set "haproxy.${backend_name}.enabled=1"
        uci add_list "haproxy.${backend_name}.server=${service} 127.0.0.1:${local_port} check"
    fi

    # Check if vhost already exists
    if uci -q get "haproxy.${vhost_name}" >/dev/null 2>&1; then
        log_warn "Vhost for '$domain' already exists"
    else
        # Create vhost in HAProxy UCI config
        log_info "Adding vhost '$domain' -> backend '$backend_name'"
        uci set "haproxy.${vhost_name}=vhost"
        uci set "haproxy.${vhost_name}.domain=${domain}"
        uci set "haproxy.${vhost_name}.backend=${backend_name}"
        uci set "haproxy.${vhost_name}.ssl=1"
        uci set "haproxy.${vhost_name}.ssl_redirect=1"
        uci set "haproxy.${vhost_name}.enabled=1"
    fi

    # Commit HAProxy UCI changes
    uci commit haproxy

    # Also save to exposure UCI for tracking
    uci set "${CONFIG_NAME}.${service}=service"
    uci set "${CONFIG_NAME}.${service}.port=$local_port"
    uci set "${CONFIG_NAME}.${service}.ssl=1"
    uci set "${CONFIG_NAME}.${service}.ssl_domain=$domain"
    uci commit "$CONFIG_NAME"

    log_ok "HAProxy UCI config updated"
    log_info "Domain: $domain -> 127.0.0.1:$local_port"

    # Regenerate and reload HAProxy
    log_info "Regenerating HAProxy config..."
    /usr/sbin/haproxyctl generate

    log_info "Reloading HAProxy..."
    /usr/sbin/haproxyctl reload

    log_ok "SSL backend configured"
    log_warn "Note: Ensure SSL certificate exists for $domain"
}

cmd_ssl_list() {
    load_config

    log_info "HAProxy SSL Backends:"
    echo ""
    printf "%-15s %-30s %-20s\n" "SERVICE" "DOMAIN" "BACKEND"
    printf "%-15s %-30s %-20s\n" "---------------" "------------------------------" "--------------------"

    # Read from HAProxy UCI config (vhosts with their backends)
    local found=0
    for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
        local domain=$(uci -q get "haproxy.${vhost}.domain")
        local backend=$(uci -q get "haproxy.${vhost}.backend")
        local enabled=$(uci -q get "haproxy.${vhost}.enabled")

        [ "$enabled" != "1" ] && continue
        [ -z "$domain" ] && continue

        # Get server from backend
        local server=""
        if [ -n "$backend" ]; then
            server=$(uci -q get "haproxy.${backend}.server" | head -1 | awk '{print $2}')
        fi

        printf "%-15s %-30s %-20s\n" "${backend:-N/A}" "$domain" "${server:-N/A}"
        found=1
    done

    [ "$found" = "0" ] && echo "  No SSL backends configured"
    echo ""
}

cmd_ssl_remove() {
    local service="$1"

    if [ -z "$service" ]; then
        log_err "Usage: secubox-exposure ssl remove <service>"
        return 1
    fi

    load_config

    # Check if haproxyctl exists
    if [ ! -x "/usr/sbin/haproxyctl" ]; then
        log_err "haproxyctl not found"
        return 1
    fi

    local backend_name="$service"
    local removed=0

    # Find and remove vhosts pointing to this backend
    for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
        local vhost_backend=$(uci -q get "haproxy.${vhost}.backend")
        if [ "$vhost_backend" = "$backend_name" ]; then
            log_info "Removing vhost '$vhost'"
            uci delete "haproxy.${vhost}"
            removed=1
        fi
    done

    # Remove backend if it exists
    if uci -q get "haproxy.${backend_name}" >/dev/null 2>&1; then
        log_info "Removing backend '$backend_name'"
        uci delete "haproxy.${backend_name}"
        removed=1
    fi

    if [ "$removed" = "0" ]; then
        log_err "No backend or vhost found for '$service'"
        return 1
    fi

    # Commit HAProxy UCI changes
    uci commit haproxy

    # Update exposure UCI
    uci delete "${CONFIG_NAME}.${service}.ssl" 2>/dev/null
    uci delete "${CONFIG_NAME}.${service}.ssl_domain" 2>/dev/null
    uci commit "$CONFIG_NAME"

    # Regenerate and reload HAProxy
    log_info "Regenerating HAProxy config..."
    /usr/sbin/haproxyctl generate

    log_info "Reloading HAProxy..."
    /usr/sbin/haproxyctl reload

    log_ok "SSL backend removed"
}

# ============================================================================
# STATUS & HELP
# ============================================================================

cmd_status() {
    load_config

    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo -e "${CYAN}            SecuBox Service Exposure Status${NC}"
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
    echo ""

    # Count services
    local total_services=$(get_listening_ports | wc -l)
    local external_services=$(get_listening_ports | grep -E "0\.0\.0\.0|::" | wc -l)

    echo -e "${BLUE}Services:${NC}"
    echo "  Total listening: $total_services"
    echo "  External (0.0.0.0): $external_services"
    echo ""

    # Tor status
    local tor_services=0
    [ -d "$TOR_HIDDEN_DIR" ] && tor_services=$(ls -1 "$TOR_HIDDEN_DIR" 2>/dev/null | wc -l)
    echo -e "${BLUE}Tor Hidden Services:${NC} $tor_services"
    if [ "$tor_services" -gt 0 ]; then
        for dir in "$TOR_HIDDEN_DIR"/*/; do
            [ -d "$dir" ] || continue
            local svc=$(basename "$dir")
            local onion=$(cat "$dir/hostname" 2>/dev/null)
            [ -n "$onion" ] && echo "  - $svc: ${onion:0:16}..."
        done
    fi
    echo ""

    # HAProxy backends (from UCI)
    local ssl_backends=0
    echo -e "${BLUE}HAProxy SSL Backends:${NC}"
    for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
        local domain=$(uci -q get "haproxy.${vhost}.domain")
        local backend=$(uci -q get "haproxy.${vhost}.backend")
        local enabled=$(uci -q get "haproxy.${vhost}.enabled")
        [ "$enabled" != "1" ] && continue
        [ -z "$domain" ] && continue
        echo "  - ${backend}: ${domain}"
        ssl_backends=$((ssl_backends + 1))
    done
    [ "$ssl_backends" = "0" ] && echo "  (none configured)"
    echo ""
    echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
}

cmd_help() {
    cat << EOF
SecuBox Service Exposure Manager

Usage: secubox-exposure <command> [options]

COMMANDS:
  scan                    Scan all listening services
  conflicts               Detect port conflicts
  fix-port <svc> [port]   Change service port (auto-assigns if no port given)
  status                  Show exposure status summary

  tor add <svc> [port]    Create Tor hidden service
  tor list                List hidden services
  tor remove <svc>        Remove hidden service
  tor sync                Sync hidden services to Tor Shield

  ssl add <svc> <domain>  Add HAProxy SSL backend
  ssl list                List SSL backends
  ssl remove <svc>        Remove SSL backend

EXAMPLES:
  secubox-exposure scan
  secubox-exposure conflicts
  secubox-exposure fix-port domoticz 8180

  secubox-exposure tor add gitea
  secubox-exposure tor add streamlit 8501 80
  secubox-exposure tor list

  secubox-exposure ssl add gitea git.example.com
  secubox-exposure ssl add streamlit app.example.com 8501
  secubox-exposure ssl list

EOF
}

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

case "$1" in
    scan)
        cmd_scan
        ;;
    conflicts)
        load_config
        cmd_conflicts
        ;;
    fix-port)
        cmd_fix_port "$2" "$3"
        ;;
    status)
        cmd_status
        ;;
    tor)
        case "$2" in
            add) cmd_tor_add "$3" "$4" "$5" ;;
            list) cmd_tor_list ;;
            remove) cmd_tor_remove "$3" ;;
            sync) cmd_tor_sync ;;
            *) log_err "Usage: secubox-exposure tor {add|list|remove|sync}"; exit 1 ;;
        esac
        ;;
    ssl)
        case "$2" in
            add) cmd_ssl_add "$3" "$4" "$5" ;;
            list) cmd_ssl_list ;;
            remove) cmd_ssl_remove "$3" ;;
            *) log_err "Usage: secubox-exposure ssl {add|list|remove}"; exit 1 ;;
        esac
        ;;
    help|--help|-h)
        cmd_help
        ;;
    *)
        cmd_help
        exit 1
        ;;
esac
