#!/bin/bash
set -e

# ============================================================
#  D3V Gateway Installer for Ubuntu/Debian
#  Reverse Proxy + WireGuard VPN
#  https://src.d3v.ac/d3v/D3V-Server
# ============================================================

INSTALL_DIR="/opt/d3v-npmwg"
COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"
CONTAINER_NAME="d3v-npmwg"
IMAGE_NAME="src.d3v.ac/d3v/d3v-server:latest"

BLOG_INSTALL_DIR="/opt/blog"
BLOG_COMPOSE_FILE="${BLOG_INSTALL_DIR}/docker-compose.yml"
BLOG_CONTAINER_NAME="d3v-blog"
BLOG_IMAGE="nginx:alpine"
BLOG_DEPLOY_USER="blogdeploy"
BLOG_DEPLOY_KEY_DIR="${BLOG_INSTALL_DIR}/deploy-keys"
BLOG_DEPLOY_PRIVATE_KEY="${BLOG_DEPLOY_KEY_DIR}/id_ed25519"
BLOG_DEPLOY_PUBLIC_KEY="${BLOG_DEPLOY_KEY_DIR}/id_ed25519.pub"

FORGEJO_INSTALL_DIR="/opt/forgejo"
FORGEJO_COMPOSE_FILE="${FORGEJO_INSTALL_DIR}/docker-compose.yml"
FORGEJO_CONTAINER_NAME="forgejo"
FORGEJO_IMAGE="codeberg.org/forgejo/forgejo:9"
DOCKER_NETWORK="d3v-net"

FORGEJO_RUNNER_DIR="/opt/forgejo-runner"
FORGEJO_RUNNER_CONTAINER="forgejo-runner"
FORGEJO_RUNNER_IMAGE="gitea/act_runner:latest"

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

# -----------------------------------------------------------
#  Helpers
# -----------------------------------------------------------
log_step()  { echo -e "${CYAN}[*]${NC} $1"; }
log_ok()    { echo -e "${GREEN}[✓]${NC} $1"; }
log_warn()  { echo -e "${YELLOW}[!]${NC} $1"; }
log_err()   { echo -e "${RED}[✗]${NC} $1"; }
separator() { echo -e "${GREEN}=================================================================${NC}"; }

self_update() {
    local remote_url="https://src.d3v.ac/d3v/D3V-Server/raw/branch/master/install.sh"
    local tmp
    tmp=$(mktemp) || return
    if curl -fsSL -m 10 "$remote_url" -o "$tmp" 2>/dev/null; then
        if ! cmp -s "$tmp" "$0"; then
            log_warn "A newer version of install.sh found. Updating..."
            cp "$tmp" "$0"
            chmod +x "$0"
            rm -f "$tmp"
            log_ok "install.sh updated. Restarting..."
            exec "$0" "$@"
            exit 0
        fi
    fi
    rm -f "$tmp"
}

require_root() {
    if [ "$(id -u)" -ne 0 ]; then
        log_err "This script must be run as root. Use: sudo $0"
        exit 1
    fi
}

get_compose_cmd() {
    if docker compose version > /dev/null 2>&1; then
        echo "docker compose"
    else
        log_err "Docker Compose plugin not found. Please install it first."
        exit 1
    fi
}

detect_public_ip() {
    local ip=""
    ip=$(curl -s -m 5 https://ifconfig.me 2>/dev/null) \
        || ip=$(curl -s -m 5 https://icanhazip.com 2>/dev/null) \
        || ip=$(curl -s -m 5 https://api.ipify.org 2>/dev/null) \
        || ip=""
    echo "$ip"
}

# -----------------------------------------------------------
#  1. Install system dependencies
# -----------------------------------------------------------
install_deps() {
    separator
    echo -e "${BOLD} Installing System Dependencies${NC}"
    separator

    # --- curl ---
    log_step "Checking curl..."
    if command -v curl > /dev/null 2>&1; then
        log_ok "curl is already installed."
    else
        log_step "Installing curl..."
        apt-get update -qq
        apt-get install -y curl
        log_ok "curl installed."
    fi

    # --- Docker ---
    log_step "Checking Docker..."
    if command -v docker > /dev/null 2>&1; then
        log_ok "Docker is already installed ($(docker --version))."
    else
        log_step "Installing Docker... (this may take 1-2 minutes)"
        curl -fsSL https://get.docker.com | sh
        log_ok "Docker installed."
    fi

    # --- Ensure Docker daemon is running ---
    log_step "Starting Docker service..."
    systemctl enable docker > /dev/null 2>&1 || true
    systemctl restart docker > /dev/null 2>&1 || true

    # --- Wait for Docker daemon ---
    log_step "Waiting for Docker daemon to be ready..."
    local retries=0
    while ! docker info > /dev/null 2>&1; do
        retries=$((retries + 1))
        if [ "$retries" -ge 30 ]; then
            log_err "Docker daemon did not start after 30 seconds."
            log_err "Try: systemctl restart docker"
            exit 1
        fi
        sleep 1
    done
    log_ok "Docker daemon is running."

    # --- Remove old standalone docker-compose (Python) if present ---
    if command -v docker-compose > /dev/null 2>&1; then
        local dc_path
        dc_path=$(command -v docker-compose)
        # Check if it's the old Python version
        if docker-compose version 2>&1 | grep -qi "docker-compose version 1"; then
            log_warn "Detected legacy docker-compose (Python). Removing it..."
            apt-get purge -y docker-compose 2>/dev/null || rm -f "$dc_path" 2>/dev/null || true
            log_ok "Legacy docker-compose removed."
        fi
    fi

    # --- Docker Compose plugin ---
    log_step "Checking Docker Compose plugin..."
    if docker compose version > /dev/null 2>&1; then
        log_ok "Docker Compose plugin is ready ($(docker compose version))."
    else
        log_step "Installing Docker Compose plugin..."
        apt-get update -qq
        apt-get install -y docker-compose-plugin
        log_ok "Docker Compose plugin installed."
    fi

    echo ""
    log_ok "All system dependencies are ready."
}

# -----------------------------------------------------------
#  x. Generate docker-compose.yml
# -----------------------------------------------------------
generate_docker_compose() {
    local host="$1"
    if [ -z "$host" ]; then
        log_err "generate_docker_compose: host is empty."
        return 1
    fi

    log_step "Generating docker-compose.yml..."

    local custom_ports_block=""
    if [ -f ".custom_ports" ]; then
        while IFS= read -r port_mapping; do
            # Ignore empty lines or comments
            [[ -z "$port_mapping" || "$port_mapping" =~ ^# ]] && continue
            custom_ports_block+="      - \"${port_mapping}\"\n"
        done < ".custom_ports"
    fi

    # Only include d3v-net if the network already exists (i.e. Forgejo is installed)
    local network_block=""
    if docker network ls --format '{{.Name}}' 2>/dev/null | grep -q "^${DOCKER_NETWORK}$"; then
        network_block="    networks:
      - d3v-net

networks:
  d3v-net:
    external: true
    name: ${DOCKER_NETWORK}"
    fi

    cat > "$COMPOSE_FILE" <<YAML
services:
  d3v-npmwg:
    image: ${IMAGE_NAME}
    container_name: ${CONTAINER_NAME}
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
    ports:
      - "80:80"       # HTTP
      - "81:81"       # Admin UI
      - "443:443"     # HTTPS
      - "51820-51830:51820-51830/udp"  # WireGuard Multi-Server Range
$(echo -e "$custom_ports_block" | sed '/^$/d')    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
      - ./wireguard:/etc/wireguard
    env_file:
      - .env
    environment:
      WG_HOST: "${host}"
${network_block}
YAML
    log_ok "docker-compose.yml created/updated."
}

# -----------------------------------------------------------
#  Save iptables rules so they persist across reboots
# -----------------------------------------------------------
save_iptables_rules() {
    if command -v netfilter-persistent > /dev/null 2>&1; then
        netfilter-persistent save > /dev/null 2>&1
        log_ok "iptables rules saved (netfilter-persistent)."
    elif command -v iptables-save > /dev/null 2>&1; then
        mkdir -p /etc/iptables
        iptables-save > /etc/iptables/rules.v4
        # Install iptables-persistent silently so rules reload on boot
        if ! dpkg -l iptables-persistent > /dev/null 2>&1; then
            log_step "Installing iptables-persistent for rule persistence..."
            DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent > /dev/null 2>&1
            log_ok "iptables-persistent installed."
        fi
        log_ok "iptables rules saved (/etc/iptables/rules.v4)."
    else
        log_warn "Could not save iptables rules — install iptables-persistent manually."
    fi
}

# -----------------------------------------------------------
#  Ensure shared Docker network exists
# -----------------------------------------------------------
ensure_docker_network() {
    if ! docker network ls --format '{{.Name}}' | grep -q "^${DOCKER_NETWORK}$"; then
        log_step "Creating shared Docker network '${DOCKER_NETWORK}'..."
        docker network create "${DOCKER_NETWORK}" > /dev/null
        log_ok "Network '${DOCKER_NETWORK}' created."
    else
        log_ok "Network '${DOCKER_NETWORK}' already exists."
    fi
}

ensure_blog_deploy_user() {
    log_step "Ensuring blog deploy user '${BLOG_DEPLOY_USER}' exists..."
    if id -u "${BLOG_DEPLOY_USER}" > /dev/null 2>&1; then
        log_ok "Deploy user '${BLOG_DEPLOY_USER}' already exists."
    else
        useradd --create-home --shell /bin/bash "${BLOG_DEPLOY_USER}"
        log_ok "Deploy user '${BLOG_DEPLOY_USER}' created."
    fi
}

ensure_blog_deploy_paths() {
    log_step "Preparing blog deploy paths..."
    mkdir -p "${BLOG_INSTALL_DIR}/public" "${BLOG_DEPLOY_KEY_DIR}"
    chown -R "${BLOG_DEPLOY_USER}:${BLOG_DEPLOY_USER}" "${BLOG_INSTALL_DIR}/public"
    chmod 755 "${BLOG_INSTALL_DIR}" "${BLOG_INSTALL_DIR}/public"
    chmod 700 "${BLOG_DEPLOY_KEY_DIR}"
    log_ok "Blog deploy paths are ready."
}

ensure_blog_deploy_keypair() {
    ensure_blog_deploy_user
    ensure_blog_deploy_paths

    if [ -f "${BLOG_DEPLOY_PRIVATE_KEY}" ] && [ -f "${BLOG_DEPLOY_PUBLIC_KEY}" ]; then
        log_ok "Blog deploy keypair already exists."
    else
        log_step "Generating blog deploy SSH keypair..."
        ssh-keygen -q -t ed25519 -C "blog-deploy@$(hostname)" -f "${BLOG_DEPLOY_PRIVATE_KEY}" -N ""
        chmod 600 "${BLOG_DEPLOY_PRIVATE_KEY}"
        chmod 644 "${BLOG_DEPLOY_PUBLIC_KEY}"
        log_ok "Blog deploy keypair generated."
    fi

    install -d -m 700 -o "${BLOG_DEPLOY_USER}" -g "${BLOG_DEPLOY_USER}" "/home/${BLOG_DEPLOY_USER}/.ssh"
    touch "/home/${BLOG_DEPLOY_USER}/.ssh/authorized_keys"
    grep -qxF "$(cat "${BLOG_DEPLOY_PUBLIC_KEY}")" "/home/${BLOG_DEPLOY_USER}/.ssh/authorized_keys" 2>/dev/null \
        || cat "${BLOG_DEPLOY_PUBLIC_KEY}" >> "/home/${BLOG_DEPLOY_USER}/.ssh/authorized_keys"
    chown "${BLOG_DEPLOY_USER}:${BLOG_DEPLOY_USER}" "/home/${BLOG_DEPLOY_USER}/.ssh/authorized_keys"
    chmod 600 "/home/${BLOG_DEPLOY_USER}/.ssh/authorized_keys"
    log_ok "Blog deploy public key installed for ${BLOG_DEPLOY_USER}."
}

get_ssh_port() {
    local ssh_port
    ssh_port=$(sshd -T 2>/dev/null | awk '/^port / {print $2; exit}')
    echo "${ssh_port:-22}"
}

show_blog_deploy_info() {
    require_root

    if [ ! -d "$BLOG_INSTALL_DIR" ]; then
        log_err "Blog is not installed. Install it first."
        return
    fi

    ensure_blog_deploy_keypair

    local deploy_host ssh_port
    deploy_host=$(detect_public_ip)
    ssh_port=$(get_ssh_port)

    separator
    echo -e "${BOLD} Blog Deploy Information${NC}"
    separator
    echo -e "  ${CYAN}BLOG_DEPLOY_HOST${NC} : ${BOLD}${deploy_host:-YOUR_SERVER_HOST}${NC}"
    echo -e "  ${CYAN}BLOG_DEPLOY_PORT${NC} : ${BOLD}${ssh_port}${NC}"
    echo -e "  ${CYAN}BLOG_DEPLOY_USER${NC} : ${BOLD}${BLOG_DEPLOY_USER}${NC}"
    echo -e "  ${CYAN}BLOG_DEPLOY_PATH${NC} : ${BOLD}${BLOG_INSTALL_DIR}${NC}"
    echo ""
    echo -e "${BOLD}  BLOG_DEPLOY_KEY${NC}"
    cat "${BLOG_DEPLOY_PRIVATE_KEY}"
    echo ""
    echo -e "${BOLD}  Server Public Key Installed In authorized_keys${NC}"
    cat "${BLOG_DEPLOY_PUBLIC_KEY}"
    echo ""
    echo -e "  Add the private key above to your Forgejo repo secret ${BOLD}BLOG_DEPLOY_KEY${NC}."
    separator
}

# -----------------------------------------------------------
#  2. Install Gateway
# -----------------------------------------------------------
do_install() {
    require_root

    if [ -d "$INSTALL_DIR" ]; then
        log_warn "Gateway is already installed at ${INSTALL_DIR}."
        log_warn "Use the Update option to pull the latest image, or Uninstall first."
        return
    fi

    separator
    echo -e "${BOLD} Gateway Installation${NC}"
    separator
    echo ""

    # --- Dependencies ---
    install_deps
    echo ""

    # --- Detect IP ---
    log_step "Detecting server public IP..."
    local detected_ip
    detected_ip=$(detect_public_ip)

    local wg_host=""
    if [ -n "$detected_ip" ]; then
        log_ok "Detected IP: ${BOLD}${detected_ip}${NC}"
        read -rp "$(echo -e "${CYAN}[?]${NC} WG_HOST [${detected_ip}]: ")" wg_host
        wg_host="${wg_host:-$detected_ip}"
    else
        log_warn "Could not auto-detect public IP."
        read -rp "$(echo -e "${CYAN}[?]${NC} Enter server public IP or domain: ")" wg_host
    fi

    if [ -z "$wg_host" ]; then
        log_err "WG_HOST cannot be empty. Aborting."
        return
    fi

    # --- Create directory ---
    log_step "Creating ${INSTALL_DIR}..."
    mkdir -p "$INSTALL_DIR"
    log_ok "Directory created."

    # --- Ensure shared network ---
    ensure_docker_network

    # --- Write docker-compose.yml ---
    generate_docker_compose "$wg_host"

    # --- Generate DB encryption key if not already present ---
    local env_file="${INSTALL_DIR}/.env"
    if [ ! -f "$env_file" ] || ! grep -q "DB_ENCRYPTION_KEY" "$env_file" 2>/dev/null; then
        log_step "Generating DB_ENCRYPTION_KEY for WireGuard key encryption..."
        local enc_key
        enc_key=$(openssl rand -hex 32)
        echo "DB_ENCRYPTION_KEY=${enc_key}" >> "$env_file"
        chmod 600 "$env_file"
        log_ok "DB_ENCRYPTION_KEY generated and saved to ${env_file}"
        log_warn "IMPORTANT: Back up ${env_file} — losing this key means WireGuard keys cannot be decrypted!"
    else
        log_ok "DB_ENCRYPTION_KEY already exists in ${env_file}."
    fi
    # --- Pull & Start ---
    log_step "Pulling Docker image (this may take a few minutes)..."
    local dc
    dc=$(get_compose_cmd)
    cd "$INSTALL_DIR"
    $dc pull
    log_ok "Image pulled."

    log_step "Starting containers..."
    $dc up -d
    log_ok "Containers started."

    # --- Verify ---
    log_step "Waiting for container to become healthy (10s)..."
    sleep 10

    if docker ps --format '{{.Names}}' | grep -q "$CONTAINER_NAME"; then
        echo ""
        separator
        echo -e "${GREEN}${BOLD}   GATEWAY INSTALLED SUCCESSFULLY!${NC}"
        separator
        echo -e "  ${CYAN}Web Admin UI${NC}  : ${BOLD}http://${wg_host}:81${NC}"
        echo -e "  ${CYAN}HTTP Proxy${NC}    : port 80"
        echo -e "  ${CYAN}HTTPS Proxy${NC}   : port 443"
        echo -e "  ${CYAN}WireGuard VPN${NC} : port 51820/udp"
        echo ""
        echo -e "  Open the Web UI in ~30s and create your admin account."
        separator
    else
        log_err "Container did not start. Check logs:"
        echo -e "  docker logs ${CONTAINER_NAME}"
    fi
}

# -----------------------------------------------------------
#  3. Uninstall Gateway
# -----------------------------------------------------------
do_uninstall() {
    require_root

    if [ ! -d "$INSTALL_DIR" ]; then
        log_warn "Gateway is not installed at ${INSTALL_DIR}."
        return
    fi

    log_warn "This will stop and remove Gateway and ALL its data."
    read -rp "$(echo -e "${RED}Are you sure? (y/N): ${NC}")" confirm
    if [[ ! "$confirm" =~ ^[yY]$ ]]; then
        echo "Cancelled."
        return
    fi

    log_step "Stopping containers..."
    local dc
    dc=$(get_compose_cmd)
    cd "$INSTALL_DIR" && $dc down -v 2>/dev/null || true
    cd /

    log_step "Removing ${INSTALL_DIR}..."
    rm -rf "$INSTALL_DIR"
    log_ok "Gateway uninstalled."
}

# -----------------------------------------------------------
#  4. Purge — uninstall app + system deps (Docker)
# -----------------------------------------------------------
do_purge() {
    require_root
    do_uninstall

    echo ""
    log_warn "Do you also want to remove Docker and Docker Compose from this system?"
    read -rp "$(echo -e "${RED}Remove Docker? (y/N): ${NC}")" confirm
    if [[ ! "$confirm" =~ ^[yY]$ ]]; then
        echo "Skipped Docker removal."
        return
    fi

    log_step "Removing Docker packages..."
    apt-get purge -y docker-ce docker-ce-cli containerd.io \
        docker-compose-plugin docker-buildx-plugin 2>/dev/null || true
    apt-get autoremove -y --purge 2>/dev/null || true
    log_ok "Docker and related packages removed."
}

# -----------------------------------------------------------
#  5. Reset admin password
# -----------------------------------------------------------
do_reset_password() {
    require_root

    if ! docker ps --format '{{.Names}}' | grep -q "$CONTAINER_NAME"; then
        log_err "Container ${CONTAINER_NAME} is not running. Start it first."
        return
    fi

    read -rsp "$(echo -e "${CYAN}[?]${NC} New password: ")" new_pass
    echo ""
    if [ -z "$new_pass" ]; then
        log_err "Password cannot be empty. Cancelled."
        return
    fi

    log_step "Resetting admin password..."

    local result
    result=$(docker exec "$CONTAINER_NAME" node -e "
        import('bcrypt').then(async bcrypt => {
            const knex = (await import('/app/db.js')).default();
            const user = await knex('user').where('is_deleted', 0).orderBy('id', 'asc').first();
            if (!user) { console.error('NO_USER'); process.exit(2); }
            const hash = await bcrypt.hash('${new_pass}', 13);
            await knex('auth').where('user_id', user.id).update({ secret: hash });
            await knex('user').where('id', user.id).update({ is_disabled: 0 });
            console.log(user.email);
            process.exit(0);
        }).catch(e => { console.error(e.message); process.exit(1); });
    " 2>&1)

    local exit_code=$?
    if [ $exit_code -eq 0 ]; then
        log_ok "Password updated successfully."
        echo -e "  Email    : ${BOLD}${result}${NC}"
    else
        log_err "Failed to reset password: ${result}"
    fi
}

# -----------------------------------------------------------
#  6. Update Gateway
# -----------------------------------------------------------
do_update() {
    require_root

    if [ ! -d "$INSTALL_DIR" ]; then
        log_err "Gateway is not installed. Install it first."
        return
    fi

    local dc
    dc=$(get_compose_cmd)
    cd "$INSTALL_DIR" || return

    # Save existing WG_HOST before regenerating template
    local current_wg_host=""
    if [ -f "docker-compose.yml" ]; then
        # Safely extract WG_HOST value ignoring quotes and spaces
        current_wg_host=$(grep -E 'WG_HOST:' docker-compose.yml | awk -F'"' '{print $2}')
    fi
    if [ -z "$current_wg_host" ]; then
        current_wg_host=$(detect_public_ip)
        log_warn "Could not extract WG_HOST. Using ${current_wg_host}."
    fi

    generate_docker_compose "$current_wg_host"

    log_step "Pulling latest image..."
    $dc pull
    log_ok "Image pulled."

    log_step "Recreating containers..."
    $dc up -d
    log_ok "Gateway updated."

    log_step "Cleaning old images..."
    docker image prune -f > /dev/null 2>&1
    log_ok "Done."
}

# -----------------------------------------------------------
#  7. Toggle Port 81 (Admin UI)
# -----------------------------------------------------------
#  x. Custom Stream Ports Manager
# -----------------------------------------------------------
do_manage_ports() {
    require_root
    echo ""
    log_step "TCP/UDP Stream Ports Manager"
    echo "If you created a Stream in Nginx Proxy Manager (e.g., listening on port 10000),"
    echo "you must expose that port down to the Docker container."
    echo ""
    
    local custom_ports_file=".custom_ports"
    touch "$custom_ports_file"
    
    echo "Current custom exposed ports:"
    if [ -s "$custom_ports_file" ]; then
        cat -n "$custom_ports_file"
    else
        echo "  (None)"
    fi
    echo ""
    
    read -rp "$(echo -e "${CYAN}[?]${NC} Enter new port mapping (e.g. 10000:10000) or 'clear' to remove all: ")" new_port
    
    if [[ "$new_port" == "clear" ]]; then
        > "$custom_ports_file"
        log_ok "All custom ports cleared."
    elif [[ -n "$new_port" ]]; then
        echo "$new_port" >> "$custom_ports_file"
        log_ok "Port $new_port added."
    else
        log_warn "No changes made."
        return
    fi
    
    log_step "Regenerating docker-compose.yml and restarting container..."
    
    local dc
    dc=$(get_compose_cmd)
    
    local current_wg_host=""
    if [ -f "docker-compose.yml" ]; then
        current_wg_host=$(grep -E 'WG_HOST:' docker-compose.yml | awk -F'"' '{print $2}')
    fi
    if [ -z "$current_wg_host" ]; then
        current_wg_host=$(detect_public_ip)
    fi

    generate_docker_compose "$current_wg_host"
    
    $dc up -d
    log_ok "Container updated with new port configurations."
}

# -----------------------------------------------------------
#  7. Toggle Port 81 (Admin UI)
# -----------------------------------------------------------
do_toggle_port_81() {
    require_root
    echo ""
    log_warn "This feature uses iptables (DOCKER-USER chain) to block external access to port 81."
    log_warn "When blocked, you can only access the Admin UI via the WireGuard VPN (http://10.8.0.1:81) or localhost."
    echo ""
    read -rp "$(echo -e "${CYAN}[?]${NC} Do you want to (B)lock or (U)nblock external access to port 81? [B/U]: ")" choice
    if [[ "$choice" =~ ^[bB]$ ]]; then
        log_step "Blocking external access to port 81..."
        # Remove existing rule if any to avoid duplicates
        iptables -D DOCKER-USER -p tcp --dport 81 -j DROP 2>/dev/null || true
        # Add rule to block port 81
        iptables -I DOCKER-USER -p tcp --dport 81 -j DROP
        log_ok "External access to port 81 is now BLOCKED."
        save_iptables_rules
    elif [[ "$choice" =~ ^[uU]$ ]]; then
        log_step "Unblocking external access to port 81..."
        iptables -D DOCKER-USER -p tcp --dport 81 -j DROP 2>/dev/null || true
        log_ok "External access to port 81 is now UNBLOCKED (Public)."
        save_iptables_rules
    else
        log_err "Invalid choice. Cancelled."
    fi
}

# -----------------------------------------------------------
#  Blog - Install / Uninstall
# -----------------------------------------------------------
do_blog_install() {
    require_root

    if [ -d "$BLOG_INSTALL_DIR" ]; then
        log_warn "Blog is already installed at ${BLOG_INSTALL_DIR}."
        log_warn "Uninstall it first if you want to recreate it."
        return
    fi

    separator
    echo -e "${BOLD} Hugo Blog Static Host Installation${NC}"
    separator
    echo ""

    install_deps
    echo ""

    ensure_docker_network
    ensure_blog_deploy_keypair

    log_step "Creating ${BLOG_INSTALL_DIR}..."
    mkdir -p "${BLOG_INSTALL_DIR}/public"
    log_ok "Directory created."

    if [ ! -f "${BLOG_INSTALL_DIR}/public/index.html" ]; then
        log_step "Creating placeholder blog homepage..."
        cat > "${BLOG_INSTALL_DIR}/public/index.html" <<HTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Blog is ready</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      max-width: 720px;
      margin: 60px auto;
      padding: 0 20px;
      line-height: 1.6;
      color: #1f2937;
      background: #f9fafb;
    }
    code {
      background: #e5e7eb;
      padding: 2px 6px;
      border-radius: 4px;
    }
  </style>
</head>
<body>
  <h1>Hugo blog host is installed</h1>
  <p>This container is ready to serve your generated Hugo site.</p>
  <p>Deploy your blog build output into <code>/opt/blog/public</code>.</p>
  <p>Then create a Proxy Host in Nginx Proxy Manager pointing to <code>${BLOG_CONTAINER_NAME}</code> on port <code>80</code>.</p>
</body>
</html>
HTML
        log_ok "Placeholder page created."
    fi

    log_step "Generating blog docker-compose.yml..."
    cat > "$BLOG_COMPOSE_FILE" <<YAML
services:
  blog:
    image: ${BLOG_IMAGE}
    container_name: ${BLOG_CONTAINER_NAME}
    restart: unless-stopped
    volumes:
      - ./public:/usr/share/nginx/html:ro
    networks:
      - d3v-net

networks:
  d3v-net:
    external: true
    name: ${DOCKER_NETWORK}
YAML
    log_ok "docker-compose.yml created."

    local dc
    dc=$(get_compose_cmd)
    cd "$BLOG_INSTALL_DIR"

    log_step "Pulling blog image..."
    $dc pull
    log_ok "Image pulled."

    log_step "Starting blog container..."
    $dc up -d
    log_ok "Blog container started."

    sleep 3

    if docker ps --format '{{.Names}}' | grep -q "^${BLOG_CONTAINER_NAME}$"; then
        echo ""
        separator
        echo -e "${GREEN}${BOLD}   BLOG INSTALLED SUCCESSFULLY!${NC}"
        separator
        echo -e "  ${CYAN}Container${NC} : ${BOLD}${BLOG_CONTAINER_NAME}${NC}"
        echo -e "  ${CYAN}Content Dir${NC}: ${BOLD}${BLOG_INSTALL_DIR}/public${NC}"
        echo -e "  ${CYAN}Deploy User${NC}: ${BOLD}${BLOG_DEPLOY_USER}${NC}"
        echo ""
        echo -e "  ${BOLD}Next steps${NC}"
        echo -e "  1. Run ${BOLD}$0 blog-deploy-info${NC} to get the Forgejo deploy secrets"
        echo -e "  2. In Nginx Proxy Manager create a Proxy Host:"
        echo -e "     Domain Names     : ${CYAN}blog.yourdomain.com${NC}"
        echo -e "     Scheme           : ${CYAN}http${NC}"
        echo -e "     Forward Hostname : ${CYAN}${BLOG_CONTAINER_NAME}${NC}"
        echo -e "     Forward Port     : ${CYAN}80${NC}"
        echo -e "  3. Request an SSL certificate and enable Force SSL"
        separator
    else
        log_err "Blog container did not start. Check logs:"
        echo -e "  docker logs ${BLOG_CONTAINER_NAME}"
    fi
}

do_blog_uninstall() {
    require_root

    if [ ! -d "$BLOG_INSTALL_DIR" ]; then
        log_warn "Blog is not installed at ${BLOG_INSTALL_DIR}."
        return
    fi

    log_warn "This will stop and remove the static blog host and ALL deployed blog files."
    read -rp "$(echo -e "${RED}Are you sure? (y/N): ${NC}")" confirm
    if [[ ! "$confirm" =~ ^[yY]$ ]]; then
        echo "Cancelled."
        return
    fi

    local dc
    dc=$(get_compose_cmd)
    cd "$BLOG_INSTALL_DIR" && $dc down -v 2>/dev/null || true
    cd /

    log_step "Removing ${BLOG_INSTALL_DIR}..."
    rm -rf "$BLOG_INSTALL_DIR"
    log_ok "Blog uninstalled."
}

do_blog_update() {
    require_root

    if [ ! -d "$BLOG_INSTALL_DIR" ]; then
        log_err "Blog is not installed. Install it first."
        return
    fi

    separator
    echo -e "${BOLD} Hugo Blog Static Host Update${NC}"
    separator
    echo ""

    install_deps
    ensure_docker_network
    ensure_blog_deploy_keypair

    local dc
    dc=$(get_compose_cmd)
    cd "$BLOG_INSTALL_DIR" || return

    log_step "Pulling latest blog image..."
    $dc pull
    log_ok "Image pulled."

    log_step "Recreating blog container..."
    $dc up -d
    log_ok "Blog updated."

    log_step "Cleaning old images..."
    docker image prune -f > /dev/null 2>&1
    log_ok "Done."
}

show_blog_menu() {
    while true; do
        echo ""
        separator
        echo -e "${BOLD}  Blog Manager${NC}"
        separator
        echo "  1) Install Blog"
        echo "  2) Update Blog"
        echo "  3) Show Deploy Info"
        echo "  4) Uninstall Blog"
        echo "  5) Back"
        separator
        read -rp "  Select [1-5]: " choice
        echo ""
        case "$choice" in
            1) do_blog_install ;;
            2) do_blog_update ;;
            3) show_blog_deploy_info ;;
            4) do_blog_uninstall ;;
            5) return ;;
            *) log_err "Invalid option." ;;
        esac
    done
}

# -----------------------------------------------------------
#  Forgejo - Install / Uninstall / Update
# -----------------------------------------------------------
do_forgejo_install() {
    require_root

    if [ -d "$FORGEJO_INSTALL_DIR" ]; then
        log_warn "Forgejo is already installed at ${FORGEJO_INSTALL_DIR}."
        log_warn "Use the Update option to pull the latest image."
        return
    fi

    separator
    echo -e "${BOLD} Forgejo Installation${NC}"
    separator
    echo ""

    install_deps
    echo ""

    ensure_docker_network

    # Ensure Gateway is on d3v-net (runtime + persistent in compose)
    if [ -f "$COMPOSE_FILE" ]; then
        if ! grep -q "d3v-net" "$COMPOSE_FILE"; then
            log_step "Updating Gateway compose to join '${DOCKER_NETWORK}'..."
            local current_wg_host=""
            current_wg_host=$(grep -E 'WG_HOST:' "$COMPOSE_FILE" | awk -F'"' '{print $2}')
            [ -z "$current_wg_host" ] && current_wg_host=$(detect_public_ip)
            cd "$INSTALL_DIR"
            generate_docker_compose "$current_wg_host"
            $(get_compose_cmd) up -d --no-recreate 2>/dev/null || true
            log_ok "Gateway compose updated."
        fi
        # Connect running container immediately (no restart needed)
        if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
            docker network connect "${DOCKER_NETWORK}" "${CONTAINER_NAME}" 2>/dev/null || true
        fi
    fi

    log_step "Creating ${FORGEJO_INSTALL_DIR}..."
    mkdir -p "${FORGEJO_INSTALL_DIR}"
    log_ok "Directory created."

    log_step "Generating Forgejo docker-compose.yml..."
    cat > "$FORGEJO_COMPOSE_FILE" <<YAML
services:
  forgejo:
    image: ${FORGEJO_IMAGE}
    container_name: ${FORGEJO_CONTAINER_NAME}
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__database__DB_TYPE=sqlite3
      - FORGEJO__server__SSH_PORT=2222
      - FORGEJO__server__SSH_LISTEN_PORT=22
      - FORGEJO__server__HTTP_PORT=3000
    volumes:
      - ./data:/data
    ports:
      - "3000:3000"
      - "2222:22"
    networks:
      - d3v-net

networks:
  d3v-net:
    external: true
    name: ${DOCKER_NETWORK}
YAML
    log_ok "docker-compose.yml created."

    local dc
    dc=$(get_compose_cmd)
    cd "$FORGEJO_INSTALL_DIR"

    log_step "Pulling Forgejo image (this may take a few minutes)..."
    $dc pull
    log_ok "Image pulled."

    log_step "Starting Forgejo..."
    $dc up -d
    log_ok "Forgejo started."

    log_step "Waiting for Forgejo to be ready (10s)..."
    sleep 10

    if docker ps --format '{{.Names}}' | grep -q "^${FORGEJO_CONTAINER_NAME}$"; then
        local server_ip
        server_ip=$(hostname -I | awk '{print $1}')

        # Block direct external access to port 3000 and 2222 (HTTPS git via NPM only)
        log_step "Blocking external access to ports 3000 and 2222..."
        iptables -D DOCKER-USER -p tcp --dport 3000 -j DROP 2>/dev/null || true
        iptables -I DOCKER-USER -p tcp --dport 3000 -j DROP
        iptables -D DOCKER-USER -p tcp --dport 2222 -j DROP 2>/dev/null || true
        iptables -I DOCKER-USER -p tcp --dport 2222 -j DROP
        log_ok "Ports 3000 and 2222 are now private (HTTPS git via NPM proxy only)."
        save_iptables_rules

        echo ""
        separator
        echo -e "${GREEN}${BOLD}   FORGEJO INSTALLED SUCCESSFULLY!${NC}"
        separator
        echo -e "  ${CYAN}Git HTTPS${NC}: via NPM proxy after hostname setup below"
        echo ""
        separator
        echo -e "${BOLD}  Add Hostname in Nginx Proxy Manager${NC}"
        separator
        echo -e "  ${YELLOW}Step 1:${NC} Open NPM Admin UI at ${BOLD}http://${server_ip}:81${NC}"
        echo ""
        echo -e "  ${YELLOW}Step 2:${NC} Go to ${BOLD}Proxy Hosts${NC} → click ${BOLD}Add Proxy Host${NC}"
        echo ""
        echo -e "  ${YELLOW}Step 3:${NC} ${BOLD}Details${NC} tab:"
        echo -e "    Domain Names     : ${CYAN}git.yourdomain.com${NC}"
        echo -e "    Scheme           : ${CYAN}http${NC}"
        echo -e "    Forward Hostname : ${CYAN}forgejo${NC}   (container name)"
        echo -e "    Forward Port     : ${CYAN}3000${NC}"
        echo -e "    [x] Cache Assets   [x] Block Common Exploits"
        echo ""
        echo -e "  ${YELLOW}Step 4:${NC} ${BOLD}SSL${NC} tab → select ${BOLD}Request a new SSL Certificate${NC}"
        echo -e "    [x] Force SSL   [x] HTTP/2 Support"
        echo ""
        echo -e "  ${YELLOW}Step 5:${NC} Click ${BOLD}Save${NC}."
        echo ""
        echo -e "  ${YELLOW}Step 6:${NC} Open ${BOLD}https://git.yourdomain.com${NC} → complete Forgejo setup"
        echo -e "    Server Domain    : ${CYAN}git.yourdomain.com${NC}"
        echo -e "    Base URL         : ${CYAN}https://git.yourdomain.com${NC}"
        echo -e "    SSH Port         : ${CYAN}2222${NC}"
        separator
    else
        log_err "Forgejo did not start. Check: docker logs ${FORGEJO_CONTAINER_NAME}"
    fi
}

do_forgejo_uninstall() {
    require_root

    if [ ! -d "$FORGEJO_INSTALL_DIR" ]; then
        log_warn "Forgejo is not installed at ${FORGEJO_INSTALL_DIR}."
        return
    fi

    log_warn "This will stop and remove Forgejo and ALL its data (repos, users, issues)."
    read -rp "$(echo -e "${RED}Are you sure? (y/N): ${NC}")" confirm
    if [[ ! "$confirm" =~ ^[yY]$ ]]; then
        echo "Cancelled."
        return
    fi

    local dc
    dc=$(get_compose_cmd)
    cd "$FORGEJO_INSTALL_DIR" && $dc down -v 2>/dev/null || true
    cd /

    log_step "Removing ${FORGEJO_INSTALL_DIR}..."
    rm -rf "$FORGEJO_INSTALL_DIR"
    log_ok "Forgejo uninstalled."

    log_step "Unblocking ports 3000 and 2222..."
    iptables -D DOCKER-USER -p tcp --dport 3000 -j DROP 2>/dev/null || true
    iptables -D DOCKER-USER -p tcp --dport 2222 -j DROP 2>/dev/null || true
    log_ok "Ports 3000 and 2222 unblocked."
    save_iptables_rules
}

do_forgejo_update() {
    require_root

    if [ ! -d "$FORGEJO_INSTALL_DIR" ]; then
        log_err "Forgejo is not installed. Install it first."
        return
    fi

    local dc
    dc=$(get_compose_cmd)
    cd "$FORGEJO_INSTALL_DIR" || return

    log_step "Pulling latest Forgejo image..."
    $dc pull
    log_ok "Image pulled."

    log_step "Recreating Forgejo container..."
    $dc up -d
    log_ok "Forgejo updated."

    log_step "Cleaning old images..."
    docker image prune -f > /dev/null 2>&1
    log_ok "Done."
}

do_forgejo_runner_install() {
    require_root

    install_deps
    echo ""

    # --- Node.js 20 (required for JS-based actions in host mode) ---
    log_step "Checking Node.js..."
    if node --version 2>/dev/null | grep -q "^v2"; then
        log_ok "Node.js $(node --version) already installed."
    else
        log_step "Installing Node.js 20 LTS..."
        curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1
        apt-get install -y nodejs > /dev/null 2>&1
        log_ok "Node.js $(node --version) installed."
    fi
    echo ""

    if docker ps -a --format '{{.Names}}' | grep -q "^${FORGEJO_RUNNER_CONTAINER}$"; then
        log_warn "Forgejo Runner is already installed."

        return
    fi

    separator
    echo -e "${BOLD} Forgejo Runner Installation${NC}"
    separator
    echo ""

    read -rp "$(echo -e "${CYAN}[?]${NC} Forgejo URL [https://src.d3v.ac]: ")" forgejo_url
    forgejo_url="${forgejo_url:-https://src.d3v.ac}"

    echo ""
    echo -e "  Get token from: ${BOLD}${forgejo_url}/-/admin/runners${NC}"
    echo -e "  Or: repo ${BOLD}Settings → Actions → Runners → Create new Runner${NC}"
    echo ""
    read -rsp "$(echo -e "${CYAN}[?]${NC} Runner registration token: ")" runner_token
    echo ""
    if [ -z "$runner_token" ]; then
        log_err "Token cannot be empty. Cancelled."
        return
    fi

    mkdir -p "$FORGEJO_RUNNER_DIR"

    # Auto-configure swap based on available RAM
    local total_ram_mb
    total_ram_mb=$(awk '/MemTotal/ { printf "%d", $2/1024 }' /proc/meminfo)
    local swap_size=""
    if [ "$total_ram_mb" -lt 2048 ]; then
        swap_size="2G"
    elif [ "$total_ram_mb" -lt 4096 ]; then
        swap_size="1G"
    fi

    if [ -n "$swap_size" ]; then
        if [ ! -f /swapfile ]; then
            log_step "RAM: ${total_ram_mb}MB detected — creating ${swap_size} swapfile..."
            fallocate -l "$swap_size" /swapfile
            chmod 600 /swapfile
            mkswap /swapfile > /dev/null
            swapon /swapfile
            echo '/swapfile none swap sw 0 0' >> /etc/fstab
            log_ok "Swapfile ${swap_size} created and enabled (persistent across reboots)."
        else
            log_ok "Swapfile already exists, skipping."
        fi
    else
        log_ok "RAM: ${total_ram_mb}MB — sufficient, no swap needed."
    fi

    # gitea/act_runner auto-registers on first start via env vars, then starts daemon
    log_step "Starting Forgejo Runner (auto-register on first boot)..."
    docker run -d \
        --name "${FORGEJO_RUNNER_CONTAINER}" \
        --restart unless-stopped \
        -e GITEA_INSTANCE_URL="${forgejo_url}" \
        -e GITEA_RUNNER_REGISTRATION_TOKEN="${runner_token}" \
        -e GITEA_RUNNER_NAME="$(hostname)-runner" \
        -e GITEA_RUNNER_LABELS="ubuntu-latest:docker://catthehacker/ubuntu:act-22.04,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04" \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v "${FORGEJO_RUNNER_DIR}:/data" \
        "${FORGEJO_RUNNER_IMAGE}"

    log_ok "Forgejo Runner started."
    echo ""
    separator
    echo -e "${GREEN}${BOLD}   FORGEJO RUNNER INSTALLED!${NC}"
    separator
    echo -e "  Runner connected to: ${BOLD}${forgejo_url}${NC}"
    echo -e "  Push a commit to trigger your first workflow."
    separator
}

do_forgejo_runner_update() {
    require_root

    if ! docker ps -a --format '{{.Names}}' | grep -q "^${FORGEJO_RUNNER_CONTAINER}$"; then
        log_err "Forgejo Runner is not installed."
        return
    fi

    log_step "Reading current Forgejo Runner configuration..."
    local runner_env
    runner_env=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "${FORGEJO_RUNNER_CONTAINER}" 2>/dev/null || true)

    local forgejo_url
    local runner_token
    local runner_name
    local runner_labels
    forgejo_url=$(echo "$runner_env" | grep '^GITEA_INSTANCE_URL=' | head -n1 | cut -d= -f2-)
    runner_token=$(echo "$runner_env" | grep '^GITEA_RUNNER_REGISTRATION_TOKEN=' | head -n1 | cut -d= -f2-)
    runner_name=$(echo "$runner_env" | grep '^GITEA_RUNNER_NAME=' | head -n1 | cut -d= -f2-)
    runner_labels=$(echo "$runner_env" | grep '^GITEA_RUNNER_LABELS=' | head -n1 | cut -d= -f2-)

    if [ -z "$forgejo_url" ] || [ -z "$runner_token" ]; then
        log_err "Could not read the existing Forgejo Runner configuration."
        log_err "Uninstall and install the runner again to re-register it."
        return
    fi

    runner_name="${runner_name:-$(hostname)-runner}"
    runner_labels="${runner_labels:-ubuntu-latest:docker://catthehacker/ubuntu:act-22.04,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04}"

    log_step "Pulling latest Forgejo Runner image..."
    docker pull "${FORGEJO_RUNNER_IMAGE}"
    log_ok "Image pulled."

    log_step "Recreating Forgejo Runner container..."
    docker stop "${FORGEJO_RUNNER_CONTAINER}" 2>/dev/null || true
    docker rm "${FORGEJO_RUNNER_CONTAINER}" 2>/dev/null || true

    mkdir -p "$FORGEJO_RUNNER_DIR"

    docker run -d \
        --name "${FORGEJO_RUNNER_CONTAINER}" \
        --restart unless-stopped \
        -e GITEA_INSTANCE_URL="${forgejo_url}" \
        -e GITEA_RUNNER_REGISTRATION_TOKEN="${runner_token}" \
        -e GITEA_RUNNER_NAME="${runner_name}" \
        -e GITEA_RUNNER_LABELS="${runner_labels}" \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v "${FORGEJO_RUNNER_DIR}:/data" \
        "${FORGEJO_RUNNER_IMAGE}" > /dev/null

    log_ok "Forgejo Runner updated."
}

do_forgejo_runner_uninstall() {
    require_root

    if ! docker ps -a --format '{{.Names}}' | grep -q "^${FORGEJO_RUNNER_CONTAINER}$"; then
        log_warn "Forgejo Runner is not installed."
        return
    fi

    log_warn "This will stop and remove the Forgejo Runner."
    read -rp "$(echo -e "${RED}Are you sure? (y/N): ${NC}")" confirm
    if [[ ! "$confirm" =~ ^[yY]$ ]]; then
        echo "Cancelled."
        return
    fi

    docker stop "${FORGEJO_RUNNER_CONTAINER}" 2>/dev/null || true
    docker rm "${FORGEJO_RUNNER_CONTAINER}" 2>/dev/null || true
    rm -rf "$FORGEJO_RUNNER_DIR"
    log_ok "Forgejo Runner uninstalled."
}

show_forgejo_menu() {
    while true; do
        echo ""
        separator
        echo -e "${BOLD}  Forgejo Manager${NC}"
        separator
        echo "  1) Install Forgejo"
        echo "  2) Uninstall Forgejo"
        echo "  3) Update Forgejo"
        echo "  4) Install Runner"
        echo "  5) Update Runner"
        echo "  6) Uninstall Runner"
        echo "  7) Back"
        separator
        read -rp "  Select [1-7]: " choice
        echo ""
        case "$choice" in
            1) do_forgejo_install ;;
            2) do_forgejo_uninstall ;;
            3) do_forgejo_update ;;
            4) do_forgejo_runner_install ;;
            5) do_forgejo_runner_update ;;
            6) do_forgejo_runner_uninstall ;;
            7) return ;;
            *) log_err "Invalid option." ;;
        esac
    done
}

# -----------------------------------------------------------
#  Gateway Manager Menu
# -----------------------------------------------------------
show_gateway_menu() {
    while true; do
        echo ""
        separator
        echo -e "${BOLD}  Gateway Manager${NC}"
        separator
        echo "  1) Install Gateway"
        echo "  2) Uninstall Gateway"
        echo "  3) Uninstall Gateway + Docker (Purge)"
        echo "  4) Reset Admin Password"
        echo "  5) Update Gateway"
        echo "  6) Manage Custom Stream Ports"
        echo "  7) Toggle Admin Port 81 (Block/Unblock)"
        echo "  8) Back"
        separator
        read -rp "  Select [1-8]: " choice
        echo ""
        case "$choice" in
            1) do_install ;;
            2) do_uninstall ;;
            3) do_purge ;;
            4) do_reset_password ;;
            5) do_update ;;
            6) do_manage_ports ;;
            7) do_toggle_port_81 ;;
            8) return ;;
            *) log_err "Invalid option." ;;
        esac
    done
}

# -----------------------------------------------------------
#  Status / Logs / Health Check
# -----------------------------------------------------------
show_container_status() {
    local label="$1"
    local name="$2"

    separator
    echo -e "${BOLD} ${label} Status${NC}"
    separator
    if docker ps -a --format '{{.Names}}' | grep -q "^${name}$"; then
        docker ps -a --filter "name=^${name}$" --format "table {{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Ports}}"
    else
        log_warn "${label} container is not installed."
    fi
}

show_service_logs() {
    local label="$1"
    local name="$2"

    separator
    echo -e "${BOLD} ${label} Logs (last 60 lines)${NC}"
    separator
    if docker ps -a --format '{{.Names}}' | grep -q "^${name}$"; then
        docker logs --tail 60 "$name" 2>&1 || true
    else
        log_warn "${label} container is not installed."
    fi
}

run_health_checks() {
    separator
    echo -e "${BOLD} Health Check Summary${NC}"
    separator

    log_step "Docker daemon"
    if docker info > /dev/null 2>&1; then
        log_ok "Docker daemon is running."
    else
        log_err "Docker daemon is not reachable."
    fi

    log_step "Shared Docker network (${DOCKER_NETWORK})"
    if docker network ls --format '{{.Name}}' | grep -q "^${DOCKER_NETWORK}$"; then
        log_ok "Network ${DOCKER_NETWORK} exists."
    else
        log_warn "Network ${DOCKER_NETWORK} does not exist."
    fi

    log_step "Gateway container"
    if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_ok "Gateway is running."
    elif docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
        log_warn "Gateway exists but is not running."
    else
        log_warn "Gateway is not installed."
    fi

    log_step "Blog container"
    if docker ps --format '{{.Names}}' | grep -q "^${BLOG_CONTAINER_NAME}$"; then
        log_ok "Blog is running."
    elif docker ps -a --format '{{.Names}}' | grep -q "^${BLOG_CONTAINER_NAME}$"; then
        log_warn "Blog exists but is not running."
    else
        log_warn "Blog is not installed."
    fi

    log_step "Forgejo container"
    if docker ps --format '{{.Names}}' | grep -q "^${FORGEJO_CONTAINER_NAME}$"; then
        log_ok "Forgejo is running."
    elif docker ps -a --format '{{.Names}}' | grep -q "^${FORGEJO_CONTAINER_NAME}$"; then
        log_warn "Forgejo exists but is not running."
    else
        log_warn "Forgejo is not installed."
    fi

    log_step "Forgejo Runner container"
    if docker ps --format '{{.Names}}' | grep -q "^${FORGEJO_RUNNER_CONTAINER}$"; then
        log_ok "Forgejo Runner is running."
    elif docker ps -a --format '{{.Names}}' | grep -q "^${FORGEJO_RUNNER_CONTAINER}$"; then
        log_warn "Forgejo Runner exists but is not running."
    else
        log_warn "Forgejo Runner is not installed."
    fi
}

show_operations_menu() {
    while true; do
        echo ""
        separator
        echo -e "${BOLD}  Status / Logs / Health Check${NC}"
        separator
        echo "  1) Gateway Status"
        echo "  2) Blog Status"
        echo "  3) Forgejo Status"
        echo "  4) Runner Status"
        echo "  5) Gateway Logs"
        echo "  6) Blog Logs"
        echo "  7) Forgejo Logs"
        echo "  8) Runner Logs"
        echo "  9) Health Check Summary"
        echo " 10) Back"
        separator
        read -rp "  Select [1-10]: " choice
        echo ""
        case "$choice" in
            1) show_container_status "Gateway" "$CONTAINER_NAME" ;;
            2) show_container_status "Blog" "$BLOG_CONTAINER_NAME" ;;
            3) show_container_status "Forgejo" "$FORGEJO_CONTAINER_NAME" ;;
            4) show_container_status "Forgejo Runner" "$FORGEJO_RUNNER_CONTAINER" ;;
            5) show_service_logs "Gateway" "$CONTAINER_NAME" ;;
            6) show_service_logs "Blog" "$BLOG_CONTAINER_NAME" ;;
            7) show_service_logs "Forgejo" "$FORGEJO_CONTAINER_NAME" ;;
            8) show_service_logs "Forgejo Runner" "$FORGEJO_RUNNER_CONTAINER" ;;
            9) run_health_checks ;;
            10) return ;;
            *) log_err "Invalid option." ;;
        esac
    done
}

# -----------------------------------------------------------
#  Interactive menu
# -----------------------------------------------------------
show_menu() {
    while true; do
        echo ""
        separator
        echo -e "${BOLD}  Server Installation Manager${NC}"
        separator
        echo "  1) Gateway"
        echo "  2) Blog"
        echo "  3) Forgejo"
        echo "  4) Status / Logs / Health Check"
        echo "  5) Exit"
        separator
        read -rp "  Select [1-5]: " choice
        echo ""
        case "$choice" in
            1) show_gateway_menu ;;
            2) show_blog_menu ;;
            3) show_forgejo_menu ;;
            4) show_operations_menu ;;
            5) echo "Bye!"; exit 0 ;;
            *) log_err "Invalid option." ;;
        esac
    done
}

show_help() {
    echo "Usage: $0 [command]"
    echo ""
    echo "Commands:"
    echo "  install      Install Gateway and dependencies"
    echo "  uninstall    Remove Gateway (keeps Docker)"
    echo "  purge        Remove Gateway AND Docker"
    echo "  reset        Reset web admin password"
    echo "  update       Pull latest image and restart"
    echo "  manage-ports Add or remove custom exposed Stream TCP/UDP ports"
    echo "  toggle-port  Block or unblock external access to Admin UI (Port 81) using iptables"
    echo "  gateway      Open Gateway submenu"
    echo "  gateway-install Install Gateway and dependencies"
    echo "  gateway-uninstall Remove Gateway (keeps Docker)"
    echo "  gateway-purge Remove Gateway AND Docker"
    echo "  gateway-reset Reset Gateway admin password"
    echo "  gateway-update Pull latest Gateway image and restart"
    echo "  blog         Open Blog submenu (install/update/uninstall)"
    echo "  blog-install Install lightweight static blog hosting for Hugo"
    echo "  blog-update  Update lightweight static blog hosting"
    echo "  blog-deploy-info Show the Blog deploy user and Forgejo secret values"
    echo "  blog-uninstall Remove lightweight static blog hosting"
    echo "  forgejo      Open Forgejo submenu (install/uninstall/update)"
    echo "  runner-update Update Forgejo Runner while keeping its configuration"
    echo "  ops          Open Status / Logs / Health Check submenu"
    echo "  help         Show this help"
    echo ""
    echo "Run without arguments to open the interactive menu."
}

# -----------------------------------------------------------
#  Entry point
# -----------------------------------------------------------
self_update "$@"

if [ "$#" -eq 0 ]; then
    show_menu
else
    case "$1" in
        install|gateway-install)     do_install ;;
        uninstall|gateway-uninstall)   do_uninstall ;;
        purge|gateway-purge)       do_purge ;;
        reset|gateway-reset)      do_reset_password ;;
        update|gateway-update)      do_update ;;
        manage-ports) do_manage_ports ;;
        toggle-port) do_toggle_port_81 ;;
        gateway) show_gateway_menu ;;
        blog) show_blog_menu ;;
        blog-install) do_blog_install ;;
        blog-update) do_blog_update ;;
        blog-deploy-info) show_blog_deploy_info ;;
        blog-uninstall) do_blog_uninstall ;;
        forgejo) show_forgejo_menu ;;
        runner-update) do_forgejo_runner_update ;;
        ops) show_operations_menu ;;
        help|-h|--help) show_help ;;
        *)
            log_err "Unknown command: $1"
            show_help
            exit 1
            ;;
    esac
fi
