#!/usr/bin/env bash
set -euo pipefail

# =============================================================================
# Changemaker Lite V2 — Configuration Wizard
# Produces a working .env file and stages the full application stack.
# =============================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
ENV_EXAMPLE="$SCRIPT_DIR/.env.example"
MKDOCS_YML="$SCRIPT_DIR/mkdocs/mkdocs.yml"
SERVICES_YAML="$SCRIPT_DIR/configs/homepage/services.yaml"

# --- Non-interactive mode flags ---
NON_INTERACTIVE=false
NI_DOMAIN=""
NI_ADMIN_EMAIL=""
NI_ADMIN_PASSWORD=""
NI_PRODUCTION=true
NI_ENABLE_ALL=false
SKIP_PORT_CHECK=false
SKIP_PANGOLIN_CHECK=false

# Systemd unit install opt-in ("", "yes", "no")
# Empty means "use default for this mode": skipped in NI unless --enable-all.
NI_INSTALL_WATCHER=""
NI_INSTALL_BACKUP=""

# SMTP flags
NI_SMTP_HOST=""
NI_SMTP_PORT=""
NI_SMTP_USER=""
NI_SMTP_PASS=""

# Pangolin flags
NI_PANGOLIN_API_URL=""
NI_PANGOLIN_API_KEY=""
NI_PANGOLIN_ORG_ID=""
NI_PANGOLIN_ENDPOINT=""
NI_PANGOLIN_SITE=""    # "new", "existing", or "" (skip)

# Service credential flags
NI_MAPBOX_KEY=""
NI_MAXMIND_ACCOUNT_ID=""
NI_MAXMIND_LICENSE_KEY=""

# CCP (Changemaker Control Panel) registration flags
NI_CCP_URL=""
NI_CCP_INVITE_CODE=""
NI_CCP_AGENT_URL=""

# --- Arg parser ---
while [[ $# -gt 0 ]]; do
  case "$1" in
    --non-interactive|-y) NON_INTERACTIVE=true; shift ;;
    --domain)       NI_DOMAIN="$2"; shift 2 ;;
    --admin-email)  NI_ADMIN_EMAIL="$2"; shift 2 ;;
    --admin-password) NI_ADMIN_PASSWORD="$2"; shift 2 ;;
    --development)  NI_PRODUCTION=false; shift ;;
    --enable-all)   NI_ENABLE_ALL=true; shift ;;
    --skip-port-check) SKIP_PORT_CHECK=true; shift ;;
    --skip-pangolin-check) SKIP_PANGOLIN_CHECK=true; shift ;;
    --install-watcher)       NI_INSTALL_WATCHER="yes"; shift ;;
    --no-install-watcher)    NI_INSTALL_WATCHER="no"; shift ;;
    --install-backup-timer)    NI_INSTALL_BACKUP="yes"; shift ;;
    --no-install-backup-timer) NI_INSTALL_BACKUP="no"; shift ;;
    # SMTP
    --smtp-host)    NI_SMTP_HOST="$2"; shift 2 ;;
    --smtp-port)    NI_SMTP_PORT="$2"; shift 2 ;;
    --smtp-user)    NI_SMTP_USER="$2"; shift 2 ;;
    --smtp-pass)    NI_SMTP_PASS="$2"; shift 2 ;;
    # Pangolin
    --pangolin-api-url)  NI_PANGOLIN_API_URL="$2"; shift 2 ;;
    --pangolin-api-key)  NI_PANGOLIN_API_KEY="$2"; shift 2 ;;
    --pangolin-org-id)   NI_PANGOLIN_ORG_ID="$2"; shift 2 ;;
    --pangolin-endpoint) NI_PANGOLIN_ENDPOINT="$2"; shift 2 ;;
    --pangolin-site)     NI_PANGOLIN_SITE="$2"; shift 2 ;;
    # Services
    --mapbox-key)         NI_MAPBOX_KEY="$2"; shift 2 ;;
    --maxmind-account-id) NI_MAXMIND_ACCOUNT_ID="$2"; shift 2 ;;
    --maxmind-license-key) NI_MAXMIND_LICENSE_KEY="$2"; shift 2 ;;
    # CCP (Changemaker Control Panel)
    --ccp-url)            NI_CCP_URL="$2"; shift 2 ;;
    --ccp-invite-code)    NI_CCP_INVITE_CODE="$2"; shift 2 ;;
    --ccp-agent-url)      NI_CCP_AGENT_URL="$2"; shift 2 ;;
    --help|-h)
      echo "Usage: bash config.sh [OPTIONS]"
      echo ""
      echo "Options:"
      echo "  --non-interactive, -y   Skip all prompts (generate secrets, use defaults)"
      echo "  --domain DOMAIN         Set domain (default: cmlite.org)"
      echo "  --admin-email EMAIL     Set admin email (default: admin@DOMAIN)"
      echo "  --admin-password PASS   Set admin password (must meet policy: 12+ chars, upper+lower+digit)"
      echo "  --development           Set NODE_ENV=development (default: production)"
      echo "  --enable-all            Enable all optional features + install systemd units"
      echo "  --skip-port-check       Skip host port availability check (not recommended)"
      echo "  --skip-pangolin-check   Skip Pangolin credential smoke test (for offline bootstrap)"
      echo ""
      echo "Systemd Units (default in -y mode: skipped, unless --enable-all):"
      echo "  --install-watcher       Install upgrade watcher systemd unit"
      echo "  --no-install-watcher    Skip upgrade watcher even with --enable-all"
      echo "  --install-backup-timer  Install daily backup timer systemd unit"
      echo "  --no-install-backup-timer  Skip backup timer even with --enable-all"
      echo ""
      echo "SMTP:"
      echo "  --smtp-host HOST        SMTP server hostname"
      echo "  --smtp-port PORT        SMTP port (default: 587)"
      echo "  --smtp-user USER        SMTP username"
      echo "  --smtp-pass PASS        SMTP password"
      echo ""
      echo "Pangolin Tunnel:"
      echo "  --pangolin-api-url URL  Pangolin REST API URL"
      echo "  --pangolin-api-key KEY  Pangolin API key"
      echo "  --pangolin-org-id ID    Pangolin organization ID"
      echo "  --pangolin-endpoint URL Pangolin dashboard/Newt WebSocket URL"
      echo "  --pangolin-site MODE    Site setup: 'new' (create) or 'existing' (connect first)"
      echo ""
      echo "Services:"
      echo "  --mapbox-key KEY        Mapbox API key for map features"
      echo "  --maxmind-account-id ID MaxMind GeoIP account ID"
      echo "  --maxmind-license-key K MaxMind GeoIP license key"
      echo ""
      echo "CCP (Changemaker Control Panel) — all 3 flags required to register:"
      echo "  --ccp-url URL           CCP server URL (e.g., https://ccp.example.com)"
      echo "  --ccp-invite-code CODE  One-time invite code from CCP"
      echo "  --ccp-agent-url URL     Agent URL the CCP reaches (e.g., https://this-host:7443)"
      echo ""
      echo "Example:"
      echo "  bash config.sh --non-interactive --domain example.org --admin-password MyStr0ngPass123"
      echo "  bash config.sh -y --domain example.org --admin-password MyStr0ngPass123 \\"
      echo "    --smtp-host smtp.example.com --smtp-port 587 --smtp-user me@example.com --smtp-pass secret \\"
      echo "    --pangolin-api-url https://api.pangolin.example/v1 --pangolin-api-key KEY \\"
      echo "    --pangolin-org-id myorg --pangolin-endpoint https://pangolin.example --pangolin-site new \\"
      echo "    --enable-all --mapbox-key pk.xxx --maxmind-account-id 12345 --maxmind-license-key abc"
      exit 0 ;;
    *) shift ;;
  esac
done

# --- Detect install mode ---
# Release mode: installed from tarball (has VERSION file, no .git directory)
# Source mode: cloned from git repository
if [[ -f "$SCRIPT_DIR/VERSION" ]] && [[ ! -d "$SCRIPT_DIR/.git" ]]; then
  INSTALL_MODE="release"
  RELEASE_VERSION=$(head -1 "$SCRIPT_DIR/VERSION")
else
  INSTALL_MODE="source"
  RELEASE_VERSION=""
fi

# --- Colors (respects NO_COLOR convention) ---
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
  RED='\033[0;31m'  GREEN='\033[0;32m'  YELLOW='\033[0;33m'
  BLUE='\033[0;34m' CYAN='\033[0;36m'   BOLD='\033[1m'
  DIM='\033[2m'     NC='\033[0m'
else
  RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' NC=''
fi

# =============================================================================
# Ensure stdin is connected to the terminal (handles curl | bash case)
# =============================================================================
if [[ "$NON_INTERACTIVE" == "false" ]]; then
  if [[ ! -t 0 ]]; then
    if [[ -e /dev/tty ]]; then
      exec 0</dev/tty
    else
      echo "[ERR] This script requires an interactive terminal." >&2
      echo "      Use --non-interactive for headless mode, or run manually:  bash config.sh" >&2
      exit 1
    fi
  fi
fi

# =============================================================================
# Utility Functions
# =============================================================================

info()    { echo -e "${CYAN}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC}   $*"; }
warn()    { echo -e "${YELLOW}[WARN]${NC} $*"; }
error()   { echo -e "${RED}[ERR]${NC}  $*" >&2; }

header() {
  echo ""
  echo -e "${BOLD}${BLUE}── $* ──${NC}"
  echo ""
}

generate_secret() {
  openssl rand -hex 32
}

generate_password() {
  local length=${1:-24}
  openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length"
}

# Line-by-line .env replacement — robust with special characters
update_env_var() {
  local key=$1
  local value=$2
  if grep -q "^${key}=" "$ENV_FILE"; then
    local tmpfile
    tmpfile=$(mktemp)
    while IFS= read -r line; do
      if [[ "$line" =~ ^${key}= ]]; then
        echo "${key}=${value}" >> "$tmpfile"
      else
        echo "$line" >> "$tmpfile"
      fi
    done < "$ENV_FILE"
    mv "$tmpfile" "$ENV_FILE"
  else
    echo "${key}=${value}" >> "$ENV_FILE"
  fi
}

backup_env_file() {
  if [[ -f "$ENV_FILE" ]]; then
    local ts
    ts=$(date +"%Y%m%d_%H%M%S")
    local backup="$ENV_FILE.backup_$ts"
    cp "$ENV_FILE" "$backup"
    success "Backed up .env to ${backup##*/}"
  fi
}

validate_password() {
  local pw=$1
  [[ ${#pw} -ge 12 ]] || return 1
  [[ "$pw" =~ [A-Z] ]] || return 1
  [[ "$pw" =~ [a-z] ]] || return 1
  [[ "$pw" =~ [0-9] ]] || return 1
  return 0
}

prompt_yes_no() {
  local prompt=$1 default=${2:-n}
  # In non-interactive mode, use default (or yes-for-all if --enable-all)
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    if [[ "$NI_ENABLE_ALL" == "true" ]]; then
      return 0  # yes to everything
    fi
    [[ "$default" == "y" ]] && return 0 || return 1
  fi
  local yn
  if [[ "$default" == "y" ]]; then
    read -rp "$prompt [Y/n]: " yn
    [[ "$yn" =~ ^[Nn]$ ]] && return 1 || return 0
  else
    read -rp "$prompt [y/N]: " yn
    [[ "$yn" =~ ^[Yy]$ ]] && return 0 || return 1
  fi
}

# =============================================================================
# Prerequisites
# =============================================================================

check_prerequisites() {
  header "Checking Prerequisites"
  local ok=true

  if command -v docker &>/dev/null; then
    success "Docker found: $(docker --version | head -1)"
  else
    error "Docker is not installed. See https://docs.docker.com/get-docker/"
    ok=false
  fi

  if docker compose version &>/dev/null; then
    success "Docker Compose found: $(docker compose version --short)"
  else
    error "Docker Compose v2 plugin not found. See https://docs.docker.com/compose/install/"
    ok=false
  fi

  if command -v openssl &>/dev/null; then
    success "OpenSSL found"
  else
    error "OpenSSL is not installed (needed for secret generation)"
    ok=false
  fi

  # Host port availability check — catches cockpit on :9090, other stray listeners.
  # Not fatal on its own; we warn here and let validate-env.sh surface details later.
  if command -v ss &>/dev/null; then
    local host_conflicts=""
    for port in 3000 4000 4100 5433 3001 3030 9090 8091 8025 9001 5678 8888; do
      if ss -Htln 2>/dev/null | awk -v p=":$port" '$4 ~ p"$" {found=1} END{exit !found}'; then
        host_conflicts+="$port "
      fi
    done
    if [[ -n "$host_conflicts" ]]; then
      warn "Host ports already in use: $host_conflicts"
      warn "This will break 'docker compose up -d' on affected services."
      warn "Common: cockpit.socket owns :9090 — 'sudo systemctl disable --now cockpit.socket'"
      warn "Run './scripts/validate-env.sh' after setup for a full report."
      if [[ "$NON_INTERACTIVE" == "true" ]]; then
        error "Refusing to continue in non-interactive mode with host port conflicts."
        error "Free the ports or pass --skip-port-check to override."
        [[ "$SKIP_PORT_CHECK" != "true" ]] && ok=false
      fi
    else
      success "Host ports available"
    fi
  else
    info "ss not installed — skipping host port check"
  fi

  $ok || { echo ""; error "Missing prerequisites. Install them and re-run."; exit 1; }
}

# =============================================================================
# Banner
# =============================================================================

print_banner() {
  cat << 'EOF'

  ██████╗██╗  ██╗ █████╗ ███╗   ██╗ ██████╗ ███████╗
 ██╔════╝██║  ██║██╔══██╗████╗  ██║██╔════╝ ██╔════╝
 ██║     ███████║███████║██╔██╗ ██║██║  ███╗█████╗
 ██║     ██╔══██║██╔══██║██║╚██╗██║██║   ██║██╔══╝
 ╚██████╗██║  ██║██║  ██║██║ ╚████║╚██████╔╝███████╗
  ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝ ╚══════╝

 ███╗   ███╗ █████╗ ██╗  ██╗███████╗██████╗
 ████╗ ████║██╔══██╗██║ ██╔╝██╔════╝██╔══██╗
 ██╔████╔██║███████║█████╔╝ █████╗  ██████╔╝
 ██║╚██╔╝██║██╔══██║██╔═██╗ ██╔══╝  ██╔══██╗
 ██║ ╚═╝ ██║██║  ██║██║  ██╗███████╗██║  ██║
 ╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝
                     V2 Configuration Wizard
EOF
  echo ""
  info "This wizard will create your .env file, generate secure secrets,"
  info "and prepare your system to run the full Changemaker Lite stack."
  echo ""
}

# =============================================================================
# .env Initialization
# =============================================================================

initialize_env() {
  header "Environment File Setup"

  if [[ ! -f "$ENV_EXAMPLE" ]]; then
    error ".env.example not found at $ENV_EXAMPLE"
    error "Make sure you're running this from the project root."
    exit 1
  fi

  RECONFIGURE_MODE=false

  if [[ -f "$ENV_FILE" ]]; then
    if [[ "$NON_INTERACTIVE" == "true" ]]; then
      # Non-interactive: back up and create fresh
      backup_env_file
      cp "$ENV_EXAMPLE" "$ENV_FILE"
      success "Created fresh .env from .env.example (backed up existing)"
    else
      warn "Existing .env file found at $ENV_FILE"
      if prompt_yes_no "Back up existing .env and create a fresh one?"; then
        backup_env_file
        cp "$ENV_EXAMPLE" "$ENV_FILE"
        success "Created fresh .env from .env.example"
      else
        info "Keeping existing .env. Existing secrets will be preserved."
        RECONFIGURE_MODE=true
      fi
    fi
  else
    cp "$ENV_EXAMPLE" "$ENV_FILE"
    success "Created .env from .env.example"
  fi
}

# =============================================================================
# Configuration Sections
# =============================================================================

configure_domain() {
  header "Domain Configuration"

  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    domain="${NI_DOMAIN:-cmlite.org}"
  else
    info "Root domain serves MkDocs documentation."
    info "All application routes are at app.<domain>."
    echo ""
    read -rp "Enter your domain (e.g., example.org) [default: cmlite.org]: " domain
    domain=${domain:-cmlite.org}
  fi

  update_env_var "DOMAIN" "$domain"
  update_env_var "BASE_DOMAIN" "https://$domain"
  update_env_var "GITEA_ROOT_URL" "https://git.$domain"
  update_env_var "GITEA_DOMAIN" "git.$domain"
  update_env_var "N8N_HOST" "n8n.$domain"
  update_env_var "SMTP_FROM" "noreply@$domain"
  update_env_var "SMTP_FROM_NAME" "Changemaker Lite"
  update_env_var "INITIAL_ADMIN_EMAIL" "admin@$domain"
  update_env_var "NC_ADMIN_EMAIL" "admin@$domain"
  update_env_var "TEST_EMAIL_RECIPIENT" "admin@$domain"
  update_env_var "EXCALIDRAW_WS_URL" "wss://draw.$domain"
  update_env_var "LISTMONK_SMTP_FROM" "Changemaker Lite <noreply@$domain>"
  update_env_var "HOMEPAGE_VAR_BASE_URL" "https://$domain"
  update_env_var "VAULTWARDEN_DOMAIN" "https://vault.$domain"
  update_env_var "GANCIO_BASE_URL" "https://events.$domain"

  # Update mkdocs.yml
  if [[ -f "$MKDOCS_YML" ]]; then
    sed -i "s|^site_url:.*|site_url: https://$domain|" "$MKDOCS_YML"
    sed -i "s|^repo_url:.*|repo_url: https://git.$domain/admin/changemaker.lite|" "$MKDOCS_YML"
    success "Updated mkdocs.yml (site_url, repo_url)"
  else
    warn "mkdocs.yml not found — skipping"
  fi

  success "Domain set to: $domain"

  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    if [[ "$NI_PRODUCTION" == "true" ]]; then
      update_env_var "NODE_ENV" "production"
      IS_PRODUCTION="yes"
    else
      IS_PRODUCTION="no"
    fi
  else
    echo ""
    if prompt_yes_no "Is this a production deployment?"; then
      update_env_var "NODE_ENV" "production"
      success "NODE_ENV set to production"
      IS_PRODUCTION="yes"
    else
      info "NODE_ENV stays as development"
      IS_PRODUCTION="no"
    fi
  fi

  # Store for later use
  CONFIGURED_DOMAIN="$domain"
}

configure_admin() {
  header "Admin Credentials"

  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    local admin_email="${NI_ADMIN_EMAIL:-admin@${CONFIGURED_DOMAIN:-cmlite.org}}"
    local admin_password="${NI_ADMIN_PASSWORD:-}"

    # Generate a password if not provided
    local password_was_generated=false
    if [[ -z "$admin_password" ]]; then
      admin_password="CmLite$(openssl rand -base64 12 | tr -dc 'a-zA-Z0-9' | head -c 12)1"
      password_was_generated=true
      info "Generated admin password: $admin_password"
      info "SAVE THIS — it will not be shown again."
    fi

    if ! validate_password "$admin_password"; then
      error "Admin password does not meet policy (12+ chars, uppercase + lowercase + digit)"
      exit 1
    fi

    update_env_var "INITIAL_ADMIN_EMAIL" "$admin_email"
    update_env_var "INITIAL_ADMIN_PASSWORD" "$admin_password"
    update_env_var "N8N_USER_EMAIL" "$admin_email"
    CONFIGURED_ADMIN_EMAIL="$admin_email"

    # Persist auto-generated password to a locked-down file so users piping
    # config.sh output (or missing the scroll) don't lose it forever.
    # Only written when we generated it — if the user supplied --admin-password,
    # they already have it.
    if [[ "$password_was_generated" == "true" ]]; then
      mkdir -p "$SCRIPT_DIR/data"
      local creds_file="$SCRIPT_DIR/data/admin-credentials.txt"
      umask 077
      cat > "$creds_file" << CREDS_EOF
# Changemaker Lite — auto-generated admin credentials
# Written by config.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
# DELETE THIS FILE after you have saved the password elsewhere.
email=$admin_email
password=$admin_password
CREDS_EOF
      chmod 600 "$creds_file"
      info "Credentials saved to: $creds_file (mode 0600)"
    fi

    success "Admin credentials configured ($admin_email)"
  else
    local default_email="admin@${CONFIGURED_DOMAIN:-cmlite.org}"
    read -rp "Admin email [default: $default_email]: " admin_email
    admin_email=${admin_email:-$default_email}

    local admin_password=""
    while true; do
      echo ""
      read -rsp "Admin password (min 12 chars, uppercase + lowercase + digit): " admin_password
      echo ""

      if validate_password "$admin_password"; then
        read -rsp "Confirm password: " confirm_password
        echo ""
        if [[ "$admin_password" == "$confirm_password" ]]; then
          break
        else
          warn "Passwords do not match. Try again."
        fi
      else
        warn "Password must be 12+ characters with uppercase, lowercase, and a digit."
      fi
    done

    update_env_var "INITIAL_ADMIN_EMAIL" "$admin_email"
    update_env_var "INITIAL_ADMIN_PASSWORD" "$admin_password"
    update_env_var "N8N_USER_EMAIL" "$admin_email"
    CONFIGURED_ADMIN_EMAIL="$admin_email"
    success "Admin credentials configured"
  fi
}

# Check if a key already has a real value in .env (non-empty, not a placeholder)
env_var_is_set() {
  local key=$1
  local val
  val=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
  [[ -n "$val" && "$val" != "changeme" && "$val" != *"example"* && "$val" != *"CHANGEME"* ]]
}

# Update an env var only if not already set (for reconfigure mode)
update_env_var_if_empty() {
  local key=$1
  local value=$2
  if [[ "$RECONFIGURE_MODE" == "true" ]] && env_var_is_set "$key"; then
    return 1  # signal: kept existing
  fi
  update_env_var "$key" "$value"
  return 0
}

generate_all_secrets() {
  header "Generating Secrets"

  if [[ "$RECONFIGURE_MODE" == "true" ]]; then
    info "Reconfigure mode: existing secrets will be preserved."
  else
    info "Auto-generating 22 unique secrets and passwords..."
  fi
  echo ""

  local generated=0 kept=0

  # JWT & Encryption (64-char hex)
  local jwt_access jwt_refresh jwt_invite enc_key
  jwt_access=$(generate_secret)
  jwt_refresh=$(generate_secret)
  jwt_invite=$(generate_secret)
  enc_key=$(generate_secret)

  local jwt_changed=false
  update_env_var_if_empty "JWT_ACCESS_SECRET" "$jwt_access" && jwt_changed=true
  update_env_var_if_empty "JWT_REFRESH_SECRET" "$jwt_refresh" && jwt_changed=true
  update_env_var_if_empty "JWT_INVITE_SECRET" "$jwt_invite" && jwt_changed=true
  update_env_var_if_empty "ENCRYPTION_KEY" "$enc_key" && jwt_changed=true
  if [[ "$jwt_changed" == "true" ]]; then
    success "JWT secrets + encryption key"
    ((generated+=4))
  else
    info "JWT secrets + encryption key (kept existing)"
    ((kept+=4))
  fi

  # Gitea SSO + service password salt (isolated from JWT secrets)
  local sso_secret svc_salt
  sso_secret=$(generate_secret)
  svc_salt=$(generate_secret)
  local sso_changed=false
  update_env_var_if_empty "GITEA_SSO_SECRET" "$sso_secret" && sso_changed=true
  update_env_var_if_empty "SERVICE_PASSWORD_SALT" "$svc_salt" && sso_changed=true
  if [[ "$sso_changed" == "true" ]]; then
    success "Gitea SSO secret + service password salt"
    ((generated+=2))
  else
    info "Gitea SSO secret + service password salt (kept existing)"
    ((kept+=2))
  fi

  # Database passwords (24-char alphanum)
  local pg_pass redis_pass
  pg_pass=$(generate_password 24)
  redis_pass=$(generate_password 24)

  local db_changed=false
  if update_env_var_if_empty "V2_POSTGRES_PASSWORD" "$pg_pass"; then
    update_env_var "DATABASE_URL" "postgresql://changemaker:${pg_pass}@localhost:5433/changemaker_v2"
    db_changed=true
  fi
  update_env_var_if_empty "REDIS_PASSWORD" "$redis_pass" && db_changed=true
  if [[ "$db_changed" == "true" ]]; then
    # Rebuild REDIS_URL if password changed
    local current_redis_pass
    current_redis_pass=$(grep "^REDIS_PASSWORD=" "$ENV_FILE" | cut -d= -f2-)
    update_env_var "REDIS_URL" "redis://:${current_redis_pass}@redis-changemaker:6379"
    success "PostgreSQL + Redis passwords"
    ((generated+=2))
  else
    info "PostgreSQL + Redis passwords (kept existing)"
    ((kept+=2))
  fi

  # Listmonk
  local lm_db_pass lm_web_pass lm_api_token
  lm_db_pass=$(generate_password 24)
  lm_web_pass=$(generate_password 20)
  lm_api_token=$(openssl rand -hex 16)

  local lm_changed=false
  update_env_var_if_empty "LISTMONK_DB_PASSWORD" "$lm_db_pass" && lm_changed=true
  update_env_var_if_empty "LISTMONK_WEB_ADMIN_PASSWORD" "$lm_web_pass" && lm_changed=true
  if update_env_var_if_empty "LISTMONK_API_TOKEN" "$lm_api_token"; then
    update_env_var "LISTMONK_ADMIN_PASSWORD" "$lm_api_token"
    lm_changed=true
  fi
  if [[ "$lm_changed" == "true" ]]; then
    success "Listmonk passwords + API token"
    ((generated+=3))
  else
    info "Listmonk passwords + API token (kept existing)"
    ((kept+=3))
  fi

  # NocoDB
  local nc_pass
  nc_pass=$(generate_password 20)
  if update_env_var_if_empty "NC_ADMIN_PASSWORD" "$nc_pass"; then
    success "NocoDB admin password"
    ((generated++))
  else
    info "NocoDB admin password (kept existing)"
    ((kept++))
  fi

  # Gitea
  local gitea_db gitea_root
  gitea_db=$(generate_password 20)
  gitea_root=$(generate_password 20)
  local gitea_changed=false
  update_env_var_if_empty "GITEA_DB_PASSWD" "$gitea_db" && gitea_changed=true
  update_env_var_if_empty "GITEA_DB_ROOT_PASSWORD" "$gitea_root" && gitea_changed=true
  if [[ "$gitea_changed" == "true" ]]; then
    success "Gitea database passwords"
    ((generated+=2))
  else
    info "Gitea database passwords (kept existing)"
    ((kept+=2))
  fi

  # n8n
  local n8n_enc n8n_pass
  n8n_enc=$(generate_password 32)
  n8n_pass=$(generate_password 20)
  local n8n_changed=false
  update_env_var_if_empty "N8N_ENCRYPTION_KEY" "$n8n_enc" && n8n_changed=true
  update_env_var_if_empty "N8N_USER_PASSWORD" "$n8n_pass" && n8n_changed=true
  if [[ "$n8n_changed" == "true" ]]; then
    success "n8n encryption key + admin password"
    ((generated+=2))
  else
    info "n8n encryption key + admin password (kept existing)"
    ((kept+=2))
  fi

  # Monitoring
  local grafana_pass gotify_pass
  grafana_pass=$(generate_password 20)
  gotify_pass=$(generate_password 20)
  local mon_changed=false
  update_env_var_if_empty "GRAFANA_ADMIN_PASSWORD" "$grafana_pass" && mon_changed=true
  update_env_var_if_empty "GOTIFY_ADMIN_PASSWORD" "$gotify_pass" && mon_changed=true
  if [[ "$mon_changed" == "true" ]]; then
    success "Grafana + Gotify admin passwords"
    ((generated+=2))
  else
    info "Grafana + Gotify admin passwords (kept existing)"
    ((kept+=2))
  fi

  # Vaultwarden
  local vw_admin_token
  vw_admin_token=$(generate_secret)
  if update_env_var_if_empty "VAULTWARDEN_ADMIN_TOKEN" "$vw_admin_token"; then
    success "Vaultwarden admin token"
    ((generated++))
  else
    info "Vaultwarden admin token (kept existing)"
    ((kept++))
  fi

  # Rocket.Chat
  local rc_pass
  rc_pass=$(generate_password 20)
  if update_env_var_if_empty "ROCKETCHAT_ADMIN_PASSWORD" "$rc_pass"; then
    success "Rocket.Chat admin password"
    ((generated++))
  else
    info "Rocket.Chat admin password (kept existing)"
    ((kept++))
  fi

  # MongoDB (required for Rocket.Chat — runs with --auth)
  local mongo_pass
  mongo_pass=$(generate_password 24)
  if update_env_var_if_empty "MONGO_ROOT_PASSWORD" "$mongo_pass"; then
    success "MongoDB root password"
    ((generated++))
  else
    info "MongoDB root password (kept existing)"
    ((kept++))
  fi

  # Gancio
  local gancio_pass
  gancio_pass=$(generate_password 20)
  if update_env_var_if_empty "GANCIO_ADMIN_PASSWORD" "$gancio_pass"; then
    success "Gancio admin password"
    ((generated++))
  else
    info "Gancio admin password (kept existing)"
    ((kept++))
  fi

  # Jitsi Meet
  local jitsi_secret jitsi_jicofo jitsi_jvb
  jitsi_secret=$(generate_secret)
  jitsi_jicofo=$(openssl rand -hex 16)
  jitsi_jvb=$(openssl rand -hex 16)
  local jitsi_changed=false
  update_env_var_if_empty "JITSI_APP_SECRET" "$jitsi_secret" && jitsi_changed=true
  update_env_var_if_empty "JITSI_JICOFO_AUTH_PASSWORD" "$jitsi_jicofo" && jitsi_changed=true
  update_env_var_if_empty "JITSI_JVB_AUTH_PASSWORD" "$jitsi_jvb" && jitsi_changed=true
  if [[ "$jitsi_changed" == "true" ]]; then
    success "Jitsi Meet secrets (JWT + XMPP)"
    ((generated+=3))
  else
    info "Jitsi Meet secrets (kept existing)"
    ((kept+=3))
  fi

  echo ""
  if [[ $kept -gt 0 ]]; then
    success "Secrets: ${generated} generated, ${kept} preserved from existing .env"
  else
    success "All 22 secrets generated. No placeholder passwords remain."
  fi
}

configure_smtp() {
  header "Email Configuration"

  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    if [[ -n "$NI_SMTP_HOST" ]]; then
      update_env_var "SMTP_HOST" "$NI_SMTP_HOST"
      update_env_var "SMTP_PORT" "${NI_SMTP_PORT:-587}"
      update_env_var "SMTP_USER" "$NI_SMTP_USER"
      update_env_var "SMTP_PASS" "$NI_SMTP_PASS"
      update_env_var "EMAIL_TEST_MODE" "false"
      update_env_var "VAULTWARDEN_SMTP_SECURITY" "starttls"
      # Also configure Listmonk SMTP
      update_env_var "LISTMONK_SMTP_HOST" "$NI_SMTP_HOST"
      update_env_var "LISTMONK_SMTP_PORT" "${NI_SMTP_PORT:-587}"
      update_env_var "LISTMONK_SMTP_USER" "$NI_SMTP_USER"
      update_env_var "LISTMONK_SMTP_PASSWORD" "$NI_SMTP_PASS"
      update_env_var "LISTMONK_SMTP_TLS_TYPE" "STARTTLS"
      success "Production SMTP configured ($NI_SMTP_HOST)"
      SMTP_MODE="production"
    else
      update_env_var "VAULTWARDEN_SMTP_SECURITY" "off"
      info "Using MailHog for email (configure SMTP later via .env)"
      SMTP_MODE="mailhog"
    fi
    return
  fi

  info "By default, emails are captured by MailHog (test mode)."
  info "You can configure a production SMTP server now or later."
  echo ""

  if prompt_yes_no "Configure production SMTP now?"; then
    read -rp "  SMTP host (e.g., smtp.protonmail.ch): " smtp_host
    read -rp "  SMTP port (e.g., 587): " smtp_port
    read -rp "  SMTP user: " smtp_user
    read -rsp "  SMTP password: " smtp_pass
    echo ""

    update_env_var "SMTP_HOST" "$smtp_host"
    update_env_var "SMTP_PORT" "$smtp_port"
    update_env_var "SMTP_USER" "$smtp_user"
    update_env_var "SMTP_PASS" "$smtp_pass"
    update_env_var "EMAIL_TEST_MODE" "false"

    if prompt_yes_no "  Also use this SMTP for Listmonk newsletters?"; then
      update_env_var "LISTMONK_SMTP_HOST" "$smtp_host"
      update_env_var "LISTMONK_SMTP_PORT" "$smtp_port"
      update_env_var "LISTMONK_SMTP_USER" "$smtp_user"
      update_env_var "LISTMONK_SMTP_PASSWORD" "$smtp_pass"
      update_env_var "LISTMONK_SMTP_TLS_TYPE" "STARTTLS"
      success "Listmonk SMTP configured"
    fi

    # Vaultwarden needs matching SMTP security
    update_env_var "VAULTWARDEN_SMTP_SECURITY" "starttls"

    success "Production SMTP configured"
    SMTP_MODE="production"
  else
    info "Using MailHog for email testing (port 8025)"
    update_env_var "VAULTWARDEN_SMTP_SECURITY" "off"
    SMTP_MODE="mailhog"
  fi
}

configure_features() {
  header "Feature Flags"

  if prompt_yes_no "Enable Media Manager (video library)?"; then
    update_env_var "ENABLE_MEDIA_FEATURES" "true"
    success "Media Manager enabled"
    MEDIA_ENABLED="yes"
  else
    MEDIA_ENABLED="no"
  fi

  if prompt_yes_no "Enable Listmonk newsletter sync?"; then
    update_env_var "LISTMONK_SYNC_ENABLED" "true"
    success "Listmonk sync enabled"
    LISTMONK_SYNC="yes"
  else
    LISTMONK_SYNC="no"
  fi

  if prompt_yes_no "Enable Payments (Stripe)?"; then
    update_env_var "ENABLE_PAYMENTS" "true"
    success "Payments enabled"
    PAYMENTS_ENABLED="yes"
  else
    PAYMENTS_ENABLED="no"
  fi

  if prompt_yes_no "Enable Rocket.Chat (team chat)?"; then
    update_env_var "ENABLE_CHAT" "true"
    success "Rocket.Chat enabled"
    CHAT_ENABLED="yes"
  else
    CHAT_ENABLED="no"
  fi

  if prompt_yes_no "Enable Gancio event sync (shift → event)?"; then
    update_env_var "GANCIO_SYNC_ENABLED" "true"
    success "Gancio sync enabled"
    GANCIO_SYNC="yes"
  else
    GANCIO_SYNC="no"
  fi

  if prompt_yes_no "Enable Jitsi Meet (video conferencing)?"; then
    update_env_var "ENABLE_MEET" "true"
    success "Jitsi Meet enabled"
    MEET_ENABLED="yes"

    if [[ "$NON_INTERACTIVE" == "false" ]]; then
      echo ""
      info "Jitsi requires your server's public IP for media traffic (NAT traversal)."
      info "Firewall must allow UDP port 10000 for video/audio."
      read -rp "  Server public IP [leave blank to set later]: " jvb_ip
      if [[ -n "$jvb_ip" ]]; then
        update_env_var "JVB_ADVERTISE_IP" "$jvb_ip"
        success "JVB advertise IP set to $jvb_ip"
      else
        warn "Set JVB_ADVERTISE_IP in .env before starting Jitsi containers."
      fi
    else
      # Non-interactive: auto-detect public IP for NAT traversal
      local detected_ip
      detected_ip=$(curl -sf --max-time 5 https://ifconfig.me 2>/dev/null || \
                    curl -sf --max-time 5 https://api.ipify.org 2>/dev/null || true)
      if [[ -n "$detected_ip" ]]; then
        update_env_var "JVB_ADVERTISE_IP" "$detected_ip"
        success "JVB advertise IP auto-detected: $detected_ip"
      else
        warn "Could not auto-detect public IP. Set JVB_ADVERTISE_IP in .env before starting Jitsi."
      fi
    fi
  else
    MEET_ENABLED="no"
  fi

  if prompt_yes_no "Enable SMS Campaigns (Termux Android bridge)?"; then
    update_env_var "ENABLE_SMS" "true"
    success "SMS Campaigns enabled"
    SMS_ENABLED="yes"

    if [[ "$NON_INTERACTIVE" == "false" ]]; then
      echo ""
      info "SMS uses a Termux-based Android phone as the sending device."
      read -rp "  Termux API URL [default: http://10.0.0.193:5001]: " termux_url
      termux_url=${termux_url:-http://10.0.0.193:5001}
      update_env_var "TERMUX_API_URL" "$termux_url"

      read -rp "  Termux API Key [leave blank to set later]: " termux_key
      if [[ -n "$termux_key" ]]; then
        update_env_var "TERMUX_API_KEY" "$termux_key"
      fi
    fi
  else
    SMS_ENABLED="no"
  fi

  if prompt_yes_no "Enable Social Connections (friendships, challenges, spotlights)?"; then
    update_env_var "ENABLE_SOCIAL" "true"
    success "Social Connections enabled"
  else
    update_env_var "ENABLE_SOCIAL" "false"
  fi

  if prompt_yes_no "Enable People CRM (unified contact management)?"; then
    update_env_var "ENABLE_PEOPLE" "true"
    success "People CRM enabled"
  else
    update_env_var "ENABLE_PEOPLE" "false"
  fi

  if prompt_yes_no "Enable Docs Comments & Version History (Gitea-backed)?"; then
    update_env_var "GITEA_COMMENTS_ENABLED" "true"
    success "Docs Comments & Version History enabled"
    DOCS_COMMENTS_ENABLED="yes"

    if [[ "$NON_INTERACTIVE" == "false" ]]; then
      echo ""
      info "Gitea will be auto-initialized on first boot (no manual install wizard)."
      info "The admin user and docs comment system will be configured automatically."
      info "Provide a password for the Gitea admin account:"
      echo ""

      read -srp "  Gitea admin password [leave blank to set up later via admin GUI]: " gitea_admin_pw
      echo ""
      if [[ -n "$gitea_admin_pw" ]]; then
        update_env_var "GITEA_ADMIN_USER" "admin"
        update_env_var "GITEA_ADMIN_PASSWORD" "$gitea_admin_pw"
        update_env_var "GITEA_COMMENTS_REPO_OWNER" "admin"
        success "Gitea admin user + docs comment auto-setup configured"
      else
        info "No password provided. Run Gitea Setup from the admin GUI after first start."
      fi
    else
      # Non-interactive: reuse admin password for Gitea
      local gitea_pw="${NI_ADMIN_PASSWORD:-}"
      if [[ -n "$gitea_pw" ]]; then
        update_env_var "GITEA_ADMIN_USER" "admin"
        update_env_var "GITEA_ADMIN_PASSWORD" "$gitea_pw"
        update_env_var "GITEA_COMMENTS_REPO_OWNER" "admin"
      fi
    fi
  else
    DOCS_COMMENTS_ENABLED="no"
  fi

  if prompt_yes_no "Enable Monitoring stack (Prometheus, Grafana, Alertmanager, cAdvisor)?" "y"; then
    local existing_profiles
    existing_profiles=$(grep -oP 'COMPOSE_PROFILES=\K.*' "$ENV_FILE" 2>/dev/null || echo "")
    if [[ -z "$existing_profiles" ]]; then
      update_env_var "COMPOSE_PROFILES" "monitoring"
    elif [[ "$existing_profiles" != *"monitoring"* ]]; then
      update_env_var "COMPOSE_PROFILES" "${existing_profiles},monitoring"
    fi
    success "Monitoring enabled (COMPOSE_PROFILES includes monitoring)"
    MONITORING_ENABLED="yes"
  else
    MONITORING_ENABLED="no"
  fi

  if prompt_yes_no "Enable Bunker Ops (fleet metrics push to central server)?"; then
    update_env_var "BUNKER_OPS_ENABLED" "true"
    success "Bunker Ops enabled"
    BUNKER_OPS_ENABLED="yes"

    if [[ "$NON_INTERACTIVE" == "false" ]]; then
      echo ""
      read -rp "  Instance label [default: domain name]: " instance_label
      if [[ -n "$instance_label" ]]; then
        update_env_var "INSTANCE_LABEL" "$instance_label"
      fi

      read -rp "  Remote write URL (VictoriaMetrics endpoint): " remote_write_url
      if [[ -n "$remote_write_url" ]]; then
        update_env_var "BUNKER_OPS_REMOTE_WRITE_URL" "$remote_write_url"
      fi
    fi
  else
    BUNKER_OPS_ENABLED="no"
  fi

  echo ""
  if prompt_yes_no "Enable Analytics & GeoIP tracking (visitor geography)?"; then
    update_env_var "ENABLE_ANALYTICS" "true"
    success "Analytics enabled"

    if [[ "$NON_INTERACTIVE" == "true" ]]; then
      if [[ -n "$NI_MAXMIND_ACCOUNT_ID" ]]; then
        update_env_var "MAXMIND_ACCOUNT_ID" "$NI_MAXMIND_ACCOUNT_ID"
        update_env_var "MAXMIND_LICENSE_KEY" "$NI_MAXMIND_LICENSE_KEY"
        success "MaxMind GeoIP credentials configured"
      fi
    else
      echo ""
      info "GeoIP tracking requires a free MaxMind account."
      info "Sign up at: https://www.maxmind.com/en/geolite2/signup"
      read -rp "  MaxMind Account ID [leave blank to set later]: " maxmind_id
      if [[ -n "$maxmind_id" ]]; then
        update_env_var "MAXMIND_ACCOUNT_ID" "$maxmind_id"
        read -rp "  MaxMind License Key: " maxmind_key
        if [[ -n "$maxmind_key" ]]; then
          update_env_var "MAXMIND_LICENSE_KEY" "$maxmind_key"
          success "MaxMind GeoIP credentials configured"
        fi
      else
        info "Set MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY in .env to enable geo tracking."
      fi
    fi
  else
    info "Analytics disabled (can enable later in admin Settings)"
  fi

  # Mapbox API key (used for map features)
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    if [[ -n "$NI_MAPBOX_KEY" ]]; then
      update_env_var "MAPBOX_API_KEY" "$NI_MAPBOX_KEY"
      success "Mapbox API key configured"
    fi
  else
    echo ""
    read -rp "  Mapbox API key [leave blank to set later]: " mapbox_key
    if [[ -n "$mapbox_key" ]]; then
      update_env_var "MAPBOX_API_KEY" "$mapbox_key"
      success "Mapbox API key configured"
    fi
  fi
}

configure_pangolin() {
  header "Tunnel Configuration (Pangolin)"

  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    if [[ -n "$NI_PANGOLIN_API_KEY" ]]; then
      local pang_url="${NI_PANGOLIN_API_URL:-https://api.bnkserve.org/v1}"
      local pang_key="$NI_PANGOLIN_API_KEY"
      local pang_org="$NI_PANGOLIN_ORG_ID"
      local pang_endpoint="${NI_PANGOLIN_ENDPOINT:-https://pangolin.bnkserve.org}"

      # Smoke-test credentials before committing to .env. Catches typos,
      # revoked keys, or wrong org IDs while recovery is still cheap.
      if [[ "$SKIP_PANGOLIN_CHECK" != "true" ]] && command -v curl &>/dev/null; then
        info "Verifying Pangolin credentials..."
        local smoke_status
        smoke_status=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
          -H "Authorization: Bearer $pang_key" \
          "$pang_url/org/$pang_org/resources" 2>/dev/null) || smoke_status="000"
        if [[ "$smoke_status" != "200" ]]; then
          error "Pangolin credentials rejected (HTTP $smoke_status)."
          error "  Check --pangolin-api-url, --pangolin-api-key, --pangolin-org-id."
          error "  URL tested: $pang_url/org/$pang_org/resources"
          error "  Pass --skip-pangolin-check to bypass (not recommended)."
          exit 1
        fi
        success "Pangolin credentials verified"
      fi

      update_env_var "PANGOLIN_API_URL" "$pang_url"
      update_env_var "PANGOLIN_API_KEY" "$pang_key"
      update_env_var "PANGOLIN_ORG_ID" "$pang_org"
      update_env_var "PANGOLIN_ENDPOINT" "$pang_endpoint"
      success "Pangolin API credentials saved"

      if command -v curl &>/dev/null && command -v jq &>/dev/null; then
        case "${NI_PANGOLIN_SITE:-}" in
          new)
            pangolin_create_site "$pang_url" "$pang_key" "$pang_org" "$pang_endpoint" ;;
          existing)
            # Connect to the first available site
            pangolin_connect_first_site "$pang_url" "$pang_key" "$pang_org" "$pang_endpoint" ;;
        esac
      fi
      PANGOLIN_CONFIGURED="yes"
    else
      info "Skipping Pangolin setup (no --pangolin-api-key provided)"
      PANGOLIN_CONFIGURED="no"
    fi
    return
  fi

  info "Pangolin provides secure public access to your services."
  info "Skip this if you'll configure tunneling later."
  echo ""

  if ! prompt_yes_no "Configure Pangolin tunnel now?"; then
    PANGOLIN_CONFIGURED="no"
    return
  fi

  read -rp "  Pangolin API URL [default: https://api.bnkserve.org/v1]: " pang_url
  pang_url=${pang_url:-https://api.bnkserve.org/v1}
  read -rp "  Pangolin API key: " pang_key
  read -rp "  Pangolin Organization ID: " pang_org

  # The Pangolin endpoint (dashboard/Newt WebSocket URL) may differ from the API URL.
  # For example: API at api.example.org vs dashboard at pangolin.example.org
  read -rp "  Pangolin Endpoint (dashboard/Newt URL) [default: https://pangolin.bnkserve.org]: " pang_endpoint
  pang_endpoint=${pang_endpoint:-https://pangolin.bnkserve.org}

  update_env_var "PANGOLIN_API_URL" "$pang_url"
  update_env_var "PANGOLIN_API_KEY" "$pang_key"
  update_env_var "PANGOLIN_ORG_ID" "$pang_org"
  update_env_var "PANGOLIN_ENDPOINT" "$pang_endpoint"

  success "Pangolin API credentials saved"

  # Check if curl and jq are available for site setup
  if ! command -v curl &>/dev/null || ! command -v jq &>/dev/null; then
    warn "curl or jq not found — skipping site setup."
    info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services."
    PANGOLIN_CONFIGURED="yes"
    return
  fi

  # Verify API connectivity
  info "Verifying Pangolin API connectivity..."
  local health_status
  health_status=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
    -H "Authorization: Bearer $pang_key" \
    "$pang_url/org/$pang_org/sites" 2>/dev/null) || true

  if [[ "$health_status" != "200" ]]; then
    warn "Could not reach Pangolin API (HTTP $health_status)."
    info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services."
    PANGOLIN_CONFIGURED="yes"
    return
  fi

  success "Pangolin API reachable"
  echo ""

  # Offer site setup options
  info "Set up a tunnel site now?"
  echo -e "  ${BOLD}1)${NC} Create a new site"
  echo -e "  ${BOLD}2)${NC} Connect to an existing site"
  echo -e "  ${BOLD}3)${NC} Skip (configure later in admin GUI)"
  echo ""
  local site_choice
  read -rp "  Choose [1-3, default: 3]: " site_choice
  site_choice=${site_choice:-3}

  case "$site_choice" in
    1) pangolin_create_site "$pang_url" "$pang_key" "$pang_org" "$pang_endpoint" ;;
    2) pangolin_connect_site "$pang_url" "$pang_key" "$pang_org" "$pang_endpoint" ;;
    *)
      info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services."
      ;;
  esac

  PANGOLIN_CONFIGURED="yes"
}

# Helper: Call Pangolin API and extract data from response envelope
pangolin_api() {
  local method=$1 url=$2 api_key=$3
  shift 3
  local body_args=()
  if [[ $# -gt 0 ]]; then
    body_args=(-d "$1")
  fi
  curl -s -m 15 -X "$method" "$url" \
    -H "Authorization: Bearer $api_key" \
    -H "Content-Type: application/json" \
    "${body_args[@]}" 2>/dev/null
}

pangolin_create_resources() {
  local api_url=$1 api_key=$2 org_id=$3 site_id=$4 domain=$5

  info "Creating Pangolin resources and targets for $domain..."

  # Look up the domain ID from registered domains
  local domains_resp domain_id
  domains_resp=$(pangolin_api GET "$api_url/org/$org_id/domains" "$api_key")
  domain_id=$(echo "$domains_resp" | jq -r --arg d "$domain" \
    '.data.domains[]? | select(.baseDomain == $d) | .domainId' 2>/dev/null)

  if [[ -z "$domain_id" ]]; then
    warn "Domain '$domain' not found in Pangolin. Register it first in the dashboard."
    info "After registering the domain, run the sync from the admin GUI at /app/pangolin."
    return
  fi

  success "Found domain: $domain (ID: $domain_id)"

  # Resource definitions: subdomain|name
  # Empty subdomain = root domain
  local -a resource_defs=(
    "app|Admin GUI"
    "api|API Server"
    "|Public Site"
    "db|NocoDB"
    "docs|Documentation"
    "code|Code Server"
    "n8n|Workflows"
    "git|Gitea"
    "home|Homepage"
    "listmonk|Newsletter"
    "qr|Mini QR"
    "draw|Excalidraw"
    "vault|Vaultwarden"
    "mail|MailHog"
    "chat|Rocket.Chat"
    "events|Gancio Events"
    "meet|Jitsi Meet"
    "grafana|Grafana"
  )

  local created=0 skipped=0 failed=0

  for def in "${resource_defs[@]}"; do
    local subdomain="${def%%|*}"
    local name="${def#*|}"
    local full_domain
    if [[ -n "$subdomain" ]]; then
      full_domain="$subdomain.$domain"
    else
      full_domain="$domain"
    fi

    # Build create payload — omit subdomain entirely for root domain (Pangolin rejects empty string)
    local create_payload
    if [[ -n "$subdomain" ]]; then
      create_payload=$(jq -n \
        --arg name "$name" \
        --arg domainId "$domain_id" \
        --arg subdomain "$subdomain" \
        '{name: $name, domainId: $domainId, subdomain: $subdomain, http: true, protocol: "tcp"}')
    else
      create_payload=$(jq -n \
        --arg name "$name" \
        --arg domainId "$domain_id" \
        '{name: $name, domainId: $domainId, http: true, protocol: "tcp"}')
    fi

    # Create the resource
    local res_resp
    res_resp=$(pangolin_api PUT "$api_url/org/$org_id/resource" "$api_key" "$create_payload")

    local resource_id
    resource_id=$(echo "$res_resp" | jq -r '.data.resourceId // empty' 2>/dev/null)

    if [[ -z "$resource_id" ]]; then
      local err_msg
      err_msg=$(echo "$res_resp" | jq -r '.message // "unknown error"' 2>/dev/null)
      if echo "$err_msg" | grep -qi "already exists\|duplicate\|conflict"; then
        skipped=$((skipped + 1))
      else
        warn "  Failed to create $full_domain: $err_msg"
        failed=$((failed + 1))
      fi
      continue
    fi

    # Create target pointing to nginx:80
    local target_payload
    target_payload=$(jq -n \
      --argjson siteId "$site_id" \
      '{siteId: $siteId, ip: "nginx", port: 80, method: "http", enabled: true}')

    pangolin_api PUT "$api_url/resource/$resource_id/target" "$api_key" "$target_payload" >/dev/null

    # Set resource as public (no SSO, no access block)
    pangolin_api POST "$api_url/resource/$resource_id" "$api_key" \
      '{"sso":false,"blockAccess":false}' >/dev/null

    created=$((created + 1))
  done

  if [[ $created -gt 0 ]]; then
    success "Created $created resources with targets → nginx:80"
  fi
  if [[ $skipped -gt 0 ]]; then
    info "$skipped resources already existed (skipped)"
  fi
  if [[ $failed -gt 0 ]]; then
    warn "$failed resources failed to create"
  fi
}

pangolin_create_site() {
  local api_url=$1 api_key=$2 org_id=$3 endpoint=$4
  local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
  local site_name

  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    site_name="changemaker-$domain"
  else
    read -rp "  Site name [default: changemaker-$domain]: " site_name
    site_name=${site_name:-changemaker-$domain}
  fi

  info "Fetching Newt credentials..."
  local defaults_resp
  defaults_resp=$(pangolin_api GET "$api_url/org/$org_id/pick-site-defaults" "$api_key")

  local newt_id newt_secret
  newt_id=$(echo "$defaults_resp" | jq -r '.data.newtId // .newtId // empty' 2>/dev/null)
  newt_secret=$(echo "$defaults_resp" | jq -r '.data.newtSecret // .newtSecret // .data.secret // .secret // empty' 2>/dev/null)

  if [[ -z "$newt_id" || -z "$newt_secret" ]]; then
    warn "Could not fetch Newt credentials from pickSiteDefaults."
    info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services."
    return
  fi

  success "Got Newt credentials (newtId: $newt_id)"

  info "Creating site \"$site_name\"..."
  local create_payload
  # Note: omit 'address' — Pangolin auto-assigns it; clientAddress from
  # pickSiteDefaults lacks CIDR notation and gets rejected.
  create_payload=$(jq -n \
    --arg name "$site_name" \
    --arg newtId "$newt_id" \
    --arg secret "$newt_secret" \
    '{name: $name, type: "newt", newtId: $newtId, secret: $secret}')

  local site_resp
  site_resp=$(pangolin_api PUT "$api_url/org/$org_id/site" "$api_key" "$create_payload")

  local site_id
  site_id=$(echo "$site_resp" | jq -r '.data.siteId // .siteId // empty' 2>/dev/null)

  if [[ -z "$site_id" ]]; then
    local err_msg
    err_msg=$(echo "$site_resp" | jq -r '.message // .error // "Unknown error"' 2>/dev/null)
    warn "Site creation failed: $err_msg"
    info "Complete tunnel setup in the admin GUI at /app/pangolin after starting services."
    return
  fi

  success "Site created: $site_id ($site_name)"

  # Write credentials to .env
  update_env_var "PANGOLIN_SITE_ID" "$site_id"
  update_env_var "PANGOLIN_NEWT_ID" "$newt_id"
  update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret"

  success "Tunnel credentials written to .env"

  # Create resources and targets
  pangolin_create_resources "$api_url" "$api_key" "$org_id" "$site_id" "$domain"
}

pangolin_connect_site() {
  local api_url=$1 api_key=$2 org_id=$3 endpoint=$4

  info "Fetching sites from organization..."
  local sites_resp
  sites_resp=$(pangolin_api GET "$api_url/org/$org_id/sites" "$api_key")

  # Extract sites array (handle Pangolin response envelope)
  local sites_json
  sites_json=$(echo "$sites_resp" | jq '.data.sites // .data // .sites // []' 2>/dev/null)

  local site_count
  site_count=$(echo "$sites_json" | jq 'length' 2>/dev/null)

  if [[ -z "$site_count" || "$site_count" -eq 0 ]]; then
    warn "No sites found in this organization."
    info "Use option 1 to create a new site, or set up via the admin GUI."
    return
  fi

  echo ""
  echo -e "  ${BOLD}Available sites:${NC}"
  echo ""

  local i=1
  while IFS= read -r line; do
    local name site_id online last_seen
    name=$(echo "$line" | jq -r '.name')
    site_id=$(echo "$line" | jq -r '.siteId')
    online=$(echo "$line" | jq -r '.online // false')
    last_seen=$(echo "$line" | jq -r '.lastSeen // "never"')

    local status_tag
    if [[ "$online" == "true" ]]; then
      status_tag="${GREEN}online${NC}"
    else
      status_tag="${DIM}offline${NC}"
    fi

    echo -e "  ${BOLD}$i)${NC} $name ${DIM}(ID: $site_id)${NC} — $status_tag"
    i=$((i + 1))
  done < <(echo "$sites_json" | jq -c '.[]')

  echo ""
  local choice
  read -rp "  Select site [1-$site_count]: " choice

  if [[ -z "$choice" || "$choice" -lt 1 || "$choice" -gt "$site_count" ]] 2>/dev/null; then
    warn "Invalid selection."
    return
  fi

  local selected
  selected=$(echo "$sites_json" | jq -c ".[$((choice - 1))]")

  local sel_id sel_name
  sel_id=$(echo "$selected" | jq -r '.siteId')
  sel_name=$(echo "$selected" | jq -r '.name')

  # Write site ID to .env
  update_env_var "PANGOLIN_SITE_ID" "$sel_id"

  success "Connected to site: $sel_name (ID: $sel_id)"

  # Fetch Newt credentials for this site from the API
  local existing_newt_id
  existing_newt_id=$(grep "^PANGOLIN_NEWT_ID=" "$ENV_FILE" 2>/dev/null | cut -d= -f2-)

  if [[ -z "$existing_newt_id" ]]; then
    info "Fetching Newt credentials for this site..."

    # Try pickSiteDefaults for fresh Newt creds
    local defaults_resp
    defaults_resp=$(pangolin_api GET "$api_url/org/$org_id/pick-site-defaults" "$api_key")
    local newt_id newt_secret
    newt_id=$(echo "$defaults_resp" | jq -r '.data.newtId // .newtId // empty' 2>/dev/null)
    newt_secret=$(echo "$defaults_resp" | jq -r '.data.newtSecret // .newtSecret // .data.secret // .secret // empty' 2>/dev/null)

    if [[ -n "$newt_id" && -n "$newt_secret" ]]; then
      update_env_var "PANGOLIN_NEWT_ID" "$newt_id"
      update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret"
      success "Newt credentials fetched and saved (newtId: $newt_id)"
    else
      info "Could not auto-fetch Newt credentials."
      info "Enter them manually (from your Pangolin dashboard)."
      echo ""
      read -rp "  Newt ID (leave blank to skip): " newt_id_input
      if [[ -n "$newt_id_input" ]]; then
        read -rp "  Newt Secret: " newt_secret_input
        if [[ -n "$newt_secret_input" ]]; then
          update_env_var "PANGOLIN_NEWT_ID" "$newt_id_input"
          update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret_input"
          success "Newt credentials written to .env"
        fi
      fi
    fi
  else
    info "Existing Newt credentials found in .env (newtId: $existing_newt_id)"
  fi

  # Create resources and targets
  local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
  pangolin_create_resources "$api_url" "$api_key" "$org_id" "$sel_id" "$domain"
}

# Non-interactive helper: connect to the first available site automatically
pangolin_connect_first_site() {
  local api_url=$1 api_key=$2 org_id=$3 endpoint=$4

  local sites_resp
  sites_resp=$(pangolin_api GET "$api_url/org/$org_id/sites" "$api_key")
  local first_site
  first_site=$(echo "$sites_resp" | jq -c '.data.sites[0] // empty' 2>/dev/null)

  if [[ -z "$first_site" || "$first_site" == "null" ]]; then
    warn "No existing sites found — cannot auto-connect."
    return
  fi

  local sel_id sel_name
  sel_id=$(echo "$first_site" | jq -r '.siteId')
  sel_name=$(echo "$first_site" | jq -r '.name')

  update_env_var "PANGOLIN_SITE_ID" "$sel_id"
  success "Connected to site: $sel_name (ID: $sel_id)"

  # Fetch Newt credentials
  local defaults_resp
  defaults_resp=$(pangolin_api GET "$api_url/org/$org_id/pick-site-defaults" "$api_key")
  local newt_id newt_secret
  newt_id=$(echo "$defaults_resp" | jq -r '.data.newtId // empty' 2>/dev/null)
  newt_secret=$(echo "$defaults_resp" | jq -r '.data.newtSecret // empty' 2>/dev/null)

  if [[ -n "$newt_id" && -n "$newt_secret" ]]; then
    update_env_var "PANGOLIN_NEWT_ID" "$newt_id"
    update_env_var "PANGOLIN_NEWT_SECRET" "$newt_secret"
    success "Newt credentials saved (newtId: $newt_id)"
  fi

  # Create resources
  local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
  pangolin_create_resources "$api_url" "$api_key" "$org_id" "$sel_id" "$domain"
}

configure_control_panel() {
  header "Control Panel Registration"

  # Non-interactive: use --ccp-* flags if all three provided, otherwise skip
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    if [[ -n "$NI_CCP_URL" && -n "$NI_CCP_INVITE_CODE" && -n "$NI_CCP_AGENT_URL" ]]; then
      update_env_var "ENABLE_CCP_AGENT" "true"
      update_env_var "CCP_URL" "$NI_CCP_URL"
      update_env_var "CCP_INVITE_CODE" "$NI_CCP_INVITE_CODE"
      update_env_var "CCP_AGENT_URL" "$NI_CCP_AGENT_URL"

      # Append ccp-agent to existing profiles (don't clobber monitoring)
      local existing_profiles
      existing_profiles=$(grep -oP 'COMPOSE_PROFILES=\K.*' "$ENV_FILE" 2>/dev/null || echo "")
      if [[ -z "$existing_profiles" ]]; then
        update_env_var "COMPOSE_PROFILES" "ccp-agent"
      elif [[ "$existing_profiles" != *"ccp-agent"* ]]; then
        update_env_var "COMPOSE_PROFILES" "${existing_profiles},ccp-agent"
      fi

      success "CCP registration configured ($NI_CCP_URL)"
    else
      update_env_var "ENABLE_CCP_AGENT" "false"
      if [[ -n "$NI_CCP_URL" || -n "$NI_CCP_INVITE_CODE" || -n "$NI_CCP_AGENT_URL" ]]; then
        warn "CCP registration needs all 3 flags: --ccp-url, --ccp-invite-code, --ccp-agent-url"
      else
        info "Skipping CCP registration (no --ccp-url provided)"
      fi
    fi
    return
  fi

  if prompt_yes_no "Register this instance with a Changemaker Control Panel?"; then
    echo ""
    read -rp "  Enter Control Panel URL (e.g., https://ccp.example.com): " ccp_url
    read -rp "  Enter invite code: " invite_code
    read -rp "  Agent URL (how the CCP reaches this host, e.g., https://this-host:7443): " agent_url

    update_env_var "ENABLE_CCP_AGENT" "true"
    update_env_var "CCP_URL" "$ccp_url"
    update_env_var "CCP_INVITE_CODE" "$invite_code"
    update_env_var "CCP_AGENT_URL" "$agent_url"

    # Add ccp-agent to compose profiles
    local existing_profiles
    existing_profiles=$(grep -oP 'COMPOSE_PROFILES=\K.*' "$ENV_FILE" 2>/dev/null || echo "")
    if [[ -n "$existing_profiles" ]]; then
      update_env_var "COMPOSE_PROFILES" "${existing_profiles},ccp-agent"
    else
      update_env_var "COMPOSE_PROFILES" "ccp-agent"
    fi

    success "Control Panel registration configured — agent will phone home on startup"
  else
    update_env_var "ENABLE_CCP_AGENT" "false"
  fi
}

configure_cors() {
  local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
  # Include app subdomain + root domain (for MkDocs payment widgets) + localhost fallbacks
  local origins="http://app.$domain,https://app.$domain,http://$domain,https://$domain,http://localhost:3000,http://localhost,http://localhost:4003"
  update_env_var "CORS_ORIGINS" "$origins"
  success "CORS origins set for $domain"
}

generate_nginx_configs() {
  header "Nginx Configuration"

  local domain="${CONFIGURED_DOMAIN:-cmlite.org}"
  local template_dir="$SCRIPT_DIR/nginx/conf.d"
  local templates_found=0

  for template in "$template_dir"/*.conf.template; do
    [[ -f "$template" ]] || continue
    templates_found=$((templates_found + 1))
    local conf_file="${template%.template}"
    local conf_name
    conf_name=$(basename "$conf_file")

    # Substitute ${DOMAIN} while preserving nginx variables ($host, $scheme, etc.)
    sed "s/\${DOMAIN}/$domain/g" "$template" > "$conf_file"
    success "Generated $conf_name"
  done

  if [[ $templates_found -eq 0 ]]; then
    warn "No nginx .conf.template files found — skipping"
  else
    success "Generated $templates_found nginx configs for domain: $domain"
    info "Restart nginx to apply: docker compose restart nginx"
  fi
}

# =============================================================================
# Homepage services.yaml
# =============================================================================

# Read a variable from .env with fallback default
read_env_var() {
  local key=$1 default=$2
  local val
  val=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
  echo "${val:-$default}"
}

generate_services_yaml() {
  local domain="${CONFIGURED_DOMAIN:-cmlite.org}"

  # Read embed proxy ports from .env (supports multi-instance port customization)
  local rc_port; rc_port=$(read_env_var ROCKETCHAT_EMBED_PORT 8891)
  local jitsi_port; jitsi_port=$(read_env_var JITSI_EMBED_PORT 8893)

  mkdir -p "$(dirname "$SERVICES_YAML")"

  cat > "$SERVICES_YAML" << YAML
---
# Homepage Services Configuration — Generated by config.sh
# Production tab: public URLs via $domain
# Local tab: localhost URLs with ports

- Production - Core:

    - Admin GUI:
        icon: mdi-view-dashboard
        href: "https://app.$domain"
        description: Application dashboard and public pages
        container: changemaker-v2-admin

    - API:
        icon: mdi-api
        href: "https://api.$domain"
        description: V2 REST API
        container: changemaker-v2-api

    - Media API:
        icon: mdi-video
        href: "https://media.$domain"
        description: Video library and streaming
        container: changemaker-media-api

    - Main Site:
        icon: mdi-web
        href: "https://$domain"
        description: Documentation and marketing site
        container: mkdocs-site-server-changemaker

- Production - Tools:

    - Code Server:
        icon: mdi-code-braces
        href: "https://code.$domain"
        description: VS Code in the browser
        container: code-server-changemaker

    - NocoDB:
        icon: mdi-database
        href: "https://db.$domain"
        description: Database browser (read-only)
        container: changemaker-v2-nocodb

    - MkDocs (Live):
        icon: mdi-book-open-page-variant
        href: "https://docs.$domain"
        description: Live documentation with hot reload
        container: mkdocs-changemaker

    - Mini QR:
        icon: mdi-qrcode
        href: "https://qr.$domain"
        description: QR code generator
        container: mini-qr

    - Excalidraw:
        icon: mdi-draw
        href: "https://draw.$domain"
        description: Collaborative whiteboard
        container: excalidraw-changemaker

    - Vaultwarden:
        icon: mdi-lock
        href: "https://vault.$domain"
        description: Password manager (Bitwarden-compatible)
        container: vaultwarden-changemaker

    - Gancio:
        icon: mdi-calendar-multiple
        href: "https://events.$domain"
        description: Event management platform
        container: gancio-changemaker

    - Rocket.Chat:
        icon: mdi-chat
        href: "https://chat.$domain"
        description: Team communication platform
        container: rocketchat-changemaker

    - Jitsi Meet:
        icon: mdi-video
        href: "https://meet.$domain"
        description: Video conferencing
        container: jitsi-web-changemaker

- Production - Integrations:

    - Listmonk:
        icon: mdi-email-newsletter
        href: "https://listmonk.$domain"
        description: Newsletter and mailing list manager
        container: listmonk-app

    - MailHog:
        icon: mdi-email-check
        href: "https://mail.$domain"
        description: Email capture for testing
        container: mailhog-changemaker

    - n8n:
        icon: mdi-robot-industrial
        href: "https://n8n.$domain"
        description: Workflow automation platform
        container: n8n-changemaker

    - Gitea:
        icon: mdi-git
        href: "https://git.$domain"
        description: Git repository hosting
        container: gitea-changemaker

- Production - Infrastructure:

    - Nginx:
        icon: mdi-web-box
        href: "#"
        description: Reverse proxy (subdomain routing)
        container: changemaker-v2-nginx

    - PostgreSQL:
        icon: mdi-database-outline
        href: "#"
        description: Primary database (V2)
        container: changemaker-v2-postgres

    - Redis:
        icon: mdi-database-sync
        href: "#"
        description: Cache, rate limiting, job queues
        container: redis-changemaker

- Production - Monitoring:

    - Grafana:
        icon: mdi-chart-box
        href: "https://grafana.$domain"
        description: Monitoring dashboards
        container: grafana-changemaker

    - Prometheus:
        icon: mdi-chart-line
        href: "https://prometheus.$domain"
        description: Metrics collection
        container: prometheus-changemaker

    - Alertmanager:
        icon: mdi-bell-alert
        href: "https://alertmanager.$domain"
        description: Alert routing
        container: alertmanager-changemaker

    - Gotify:
        icon: mdi-cellphone-message
        href: "https://gotify.$domain"
        description: Push notifications
        container: gotify-changemaker

    - cAdvisor:
        icon: mdi-docker
        href: "https://cadvisor.$domain"
        description: Container metrics
        container: cadvisor-changemaker

    - Node Exporter:
        icon: mdi-server
        href: "#"
        description: Host system metrics
        container: node-exporter-changemaker

    - Redis Exporter:
        icon: mdi-database-export
        href: "#"
        description: Redis metrics exporter
        container: redis-exporter-changemaker

# ─────────────────────────────────────────────────
# LOCAL DEVELOPMENT
# ─────────────────────────────────────────────────

- Local - Core:

    - Admin GUI:
        icon: mdi-view-dashboard
        href: "http://localhost:3000"
        description: Application dashboard (port 3000)
        container: changemaker-v2-admin

    - API:
        icon: mdi-api
        href: "http://localhost:4000"
        description: V2 REST API (port 4000)
        container: changemaker-v2-api

    - Media API:
        icon: mdi-video
        href: "http://localhost:4100"
        description: Video library API (port 4100)
        container: changemaker-media-api

    - Main Site:
        icon: mdi-web
        href: "http://localhost:4004"
        description: Documentation site (port 4004)
        container: mkdocs-site-server-changemaker

    - Homepage:
        icon: mdi-home
        href: "http://localhost:3010"
        description: This dashboard (port 3010)
        container: homepage-changemaker

- Local - Tools:

    - Code Server:
        icon: mdi-code-braces
        href: "http://localhost:8888"
        description: VS Code in the browser (port 8888)
        container: code-server-changemaker

    - NocoDB:
        icon: mdi-database
        href: "http://localhost:8091"
        description: Database browser (port 8091)
        container: changemaker-v2-nocodb

    - MkDocs (Live):
        icon: mdi-book-open-page-variant
        href: "http://localhost:4003"
        description: Live documentation (port 4003)
        container: mkdocs-changemaker

    - Mini QR:
        icon: mdi-qrcode
        href: "http://localhost:8089"
        description: QR code generator (port 8089)
        container: mini-qr

    - Excalidraw:
        icon: mdi-draw
        href: "http://localhost:8090"
        description: Collaborative whiteboard (port 8090)
        container: excalidraw-changemaker

    - Vaultwarden:
        icon: mdi-lock
        href: "http://localhost:8445"
        description: Password manager (port 8445)
        container: vaultwarden-changemaker

    - Gancio:
        icon: mdi-calendar-multiple
        href: "http://localhost:8092"
        description: Event management (port 8092)
        container: gancio-changemaker

    - Rocket.Chat:
        icon: mdi-chat
        href: "http://localhost:$rc_port"
        description: Team chat (port $rc_port)
        container: rocketchat-changemaker

    - Jitsi Meet:
        icon: mdi-video
        href: "http://localhost:$jitsi_port"
        description: Video conferencing (port $jitsi_port)
        container: jitsi-web-changemaker

- Local - Integrations:

    - Listmonk:
        icon: mdi-email-newsletter
        href: "http://localhost:9001"
        description: Newsletter manager (port 9001)
        container: listmonk-app

    - MailHog:
        icon: mdi-email-check
        href: "http://localhost:8025"
        description: Email capture UI (port 8025)
        container: mailhog-changemaker

    - n8n:
        icon: mdi-robot-industrial
        href: "http://localhost:5678"
        description: Workflow automation (port 5678)
        container: n8n-changemaker

    - Gitea:
        icon: mdi-git
        href: "http://localhost:3030"
        description: Git repository hosting (port 3030)
        container: gitea-changemaker

- Local - Infrastructure:

    - Nginx:
        icon: mdi-web-box
        href: "#"
        description: Reverse proxy (port 80)
        container: changemaker-v2-nginx

    - PostgreSQL:
        icon: mdi-database-outline
        href: "#"
        description: Primary database (port 5433)
        container: changemaker-v2-postgres

    - Redis:
        icon: mdi-database-sync
        href: "#"
        description: Cache and job queues (port 6379)
        container: redis-changemaker

- Local - Monitoring:

    - Grafana:
        icon: mdi-chart-box
        href: "http://localhost:3005"
        description: Monitoring dashboards (port 3005)
        container: grafana-changemaker

    - Prometheus:
        icon: mdi-chart-line
        href: "http://localhost:9090"
        description: Metrics collection (port 9090)
        container: prometheus-changemaker

    - Alertmanager:
        icon: mdi-bell-alert
        href: "http://localhost:9093"
        description: Alert routing (port 9093)
        container: alertmanager-changemaker

    - Gotify:
        icon: mdi-cellphone-message
        href: "http://localhost:8889"
        description: Push notifications (port 8889)
        container: gotify-changemaker

    - cAdvisor:
        icon: mdi-docker
        href: "http://localhost:8086"
        description: Container metrics (port 8086)
        container: cadvisor-changemaker

    - Node Exporter:
        icon: mdi-server
        href: "http://localhost:9100/metrics"
        description: Host system metrics (port 9100)
        container: node-exporter-changemaker

    - Redis Exporter:
        icon: mdi-database-export
        href: "http://localhost:9121/metrics"
        description: Redis metrics (port 9121)
        container: redis-exporter-changemaker
YAML

  success "Generated services.yaml for Homepage dashboard"
}

# =============================================================================
# Documentation Site Reset
# =============================================================================

configure_docs_reset() {
  header "Documentation Site"

  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    info "Keeping existing documentation content"
    return
  fi

  if prompt_yes_no "Reset documentation site to baseline? (keeps header & tracking)"; then
    touch "$SCRIPT_DIR/mkdocs/.reset-docs-on-startup"
    success "Docs reset scheduled for first startup"
    info "Custom code (header, analytics, hooks, assets) will be preserved"
  else
    info "Keeping existing documentation content"
  fi
}

# =============================================================================
# Directory Permissions
# =============================================================================

fix_container_permissions() {
  header "Container Directory Permissions"

  local -a dirs=(
    "configs/code-server/.config:Code Server config"
    "configs/code-server/.local:Code Server local data"
    "mkdocs:MkDocs root"
    "mkdocs/docs:MkDocs source docs"
    "mkdocs/overrides:MkDocs template overrides"
    "mkdocs/.cache:MkDocs cache"
    "mkdocs/site:MkDocs built site"
    "assets/uploads:Listmonk uploads"
    "assets/images:Shared images"
    "assets/icons:Homepage icons"
    "media/local/inbox:Media upload inbox"
    "media/local/photos:Media photos"
    "media/local/thumbnails:Video thumbnails"
    "media/public:Public media files"
    "local-files:n8n local files"
    "data:NAR import data"
    "data/upgrade:Upgrade trigger directory"
  )

  local errors=0
  for entry in "${dirs[@]}"; do
    local dir_path="$SCRIPT_DIR/${entry%%:*}"
    local dir_desc="${entry#*:}"

    mkdir -p "$dir_path"

    if chmod 775 "$dir_path" 2>/dev/null; then
      success "$dir_desc"
    else
      warn "$dir_desc — could not set permissions (may need sudo)"
      ((errors++))
    fi
  done

  echo ""
  if [[ $errors -eq 0 ]]; then
    success "All directories ready"
  else
    warn "Some directories may need: sudo chown -R $(id -u):$(id -g) $SCRIPT_DIR"
  fi
}

# =============================================================================
# Upgrade Watcher (systemd)
# =============================================================================

install_upgrade_watcher() {
  header "System Upgrade Watcher"

  # Ensure upgrade IPC directory exists regardless of install decision
  mkdir -p "$SCRIPT_DIR/data/upgrade"

  # Resolve whether to install in non-interactive mode:
  #   --install-watcher         => yes
  #   --no-install-watcher      => no
  #   --enable-all (no override) => yes (new default)
  #   otherwise                 => no (preserve legacy behaviour)
  local should_install="ask"
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    case "$NI_INSTALL_WATCHER" in
      yes) should_install="yes" ;;
      no)  should_install="no" ;;
      "")  if [[ "$NI_ENABLE_ALL" == "true" ]]; then should_install="yes"; else should_install="no"; fi ;;
    esac
    if [[ "$should_install" == "no" ]]; then
      info "Skipping systemd watcher install (pass --install-watcher or --enable-all to install)"
      UPGRADE_WATCHER="skipped"
      return
    fi
  fi

  info "The upgrade watcher lets you trigger upgrades from the admin Settings page."
  info "It installs a systemd path watcher that monitors for trigger files."
  echo ""

  local unit_src="$SCRIPT_DIR/scripts/systemd"
  if [[ ! -f "$unit_src/changemaker-upgrade.path" ]] || [[ ! -f "$unit_src/changemaker-upgrade.service" ]]; then
    warn "Systemd unit templates not found in scripts/systemd/ — skipping"
    UPGRADE_WATCHER="skipped"
    return
  fi

  if ! command -v systemctl &>/dev/null; then
    warn "systemctl not found — skipping (not a systemd host?)"
    UPGRADE_WATCHER="skipped"
    return
  fi

  if [[ "$should_install" == "yes" ]] || prompt_yes_no "Install the upgrade watcher (requires sudo)?"; then
    # Generate units with correct paths substituted
    local tmp_path tmp_service
    tmp_path=$(mktemp)
    tmp_service=$(mktemp)

    sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" "$unit_src/changemaker-upgrade.path" > "$tmp_path"
    sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" -e "s|__USER__|$(whoami)|g" "$unit_src/changemaker-upgrade.service" > "$tmp_service"

    if sudo cp "$tmp_path" /etc/systemd/system/changemaker-upgrade.path \
       && sudo cp "$tmp_service" /etc/systemd/system/changemaker-upgrade.service \
       && sudo systemctl daemon-reload \
       && sudo systemctl enable --now changemaker-upgrade.path; then
      success "Upgrade watcher installed and enabled"
      UPGRADE_WATCHER="yes"
    else
      warn "Failed to install systemd units (sudo may have failed)"
      warn "Install manually later:"
      echo -e "  ${CYAN}cd ~/changemaker.lite && sudo ./scripts/systemd/install.sh${NC}"
      UPGRADE_WATCHER="manual"
    fi

    rm -f "$tmp_path" "$tmp_service"
  else
    info "Skipped. You can install it later from Settings > System."
    UPGRADE_WATCHER="skipped"
  fi
}

# =============================================================================
# Automated Backups (systemd)
# =============================================================================

install_backup_timer() {
  header "Automated Backups"

  local should_install="ask"
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    case "$NI_INSTALL_BACKUP" in
      yes) should_install="yes" ;;
      no)  should_install="no" ;;
      "")  if [[ "$NI_ENABLE_ALL" == "true" ]]; then should_install="yes"; else should_install="no"; fi ;;
    esac
    if [[ "$should_install" == "no" ]]; then
      info "Skipping backup timer install (pass --install-backup-timer or --enable-all to install)"
      BACKUP_TIMER="skipped"
      return
    fi
  fi

  info "Daily automated backups protect against data loss."
  info "Backs up PostgreSQL databases + uploads to ./backups/ with 30-day retention."
  echo ""

  local unit_src="$SCRIPT_DIR/scripts/systemd"
  if [[ ! -f "$unit_src/changemaker-backup.timer" ]] || [[ ! -f "$unit_src/changemaker-backup.service" ]]; then
    warn "Systemd backup unit templates not found in scripts/systemd/ — skipping"
    BACKUP_TIMER="skipped"
    return
  fi

  if ! command -v systemctl &>/dev/null; then
    warn "systemctl not found — skipping (not a systemd host?)"
    BACKUP_TIMER="skipped"
    return
  fi

  if [[ "$should_install" == "yes" ]] || prompt_yes_no "Install daily automated backups (requires sudo)?" "y"; then
    local tmp_timer tmp_service
    tmp_timer=$(mktemp)
    tmp_service=$(mktemp)

    sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" "$unit_src/changemaker-backup.timer" > "$tmp_timer"
    sed -e "s|__PROJECT_DIR__|$SCRIPT_DIR|g" -e "s|__USER__|$(whoami)|g" "$unit_src/changemaker-backup.service" > "$tmp_service"

    if sudo cp "$tmp_timer" /etc/systemd/system/changemaker-backup.timer \
       && sudo cp "$tmp_service" /etc/systemd/system/changemaker-backup.service \
       && sudo systemctl daemon-reload \
       && sudo systemctl enable --now changemaker-backup.timer; then
      success "Daily backup timer installed and enabled (runs at 02:00)"
      BACKUP_TIMER="yes"
    else
      warn "Failed to install systemd units (sudo may have failed)"
      warn "Install manually later:"
      echo -e "  ${CYAN}sudo cp scripts/systemd/changemaker-backup.* /etc/systemd/system/${NC}"
      echo -e "  ${CYAN}sudo systemctl daemon-reload && sudo systemctl enable --now changemaker-backup.timer${NC}"
      BACKUP_TIMER="manual"
    fi

    rm -f "$tmp_timer" "$tmp_service"
  else
    info "Skipped. Run backups manually: ./scripts/backup.sh"
    BACKUP_TIMER="skipped"
  fi
}

# =============================================================================
# Summary & Next Steps
# =============================================================================

print_summary() {
  header "Configuration Complete"

  echo -e "  ${BOLD}Domain:${NC}          ${CONFIGURED_DOMAIN:-cmlite.org}"
  echo -e "  ${BOLD}Admin email:${NC}     ${CONFIGURED_ADMIN_EMAIL:-admin@${CONFIGURED_DOMAIN:-cmlite.org}}"
  echo -e "  ${BOLD}Admin password:${NC}  [set]"
  echo -e "  ${BOLD}SMTP:${NC}            ${SMTP_MODE:-mailhog}"
  echo -e "  ${BOLD}Media Manager:${NC}   ${MEDIA_ENABLED:-no}"
  echo -e "  ${BOLD}Payments:${NC}        ${PAYMENTS_ENABLED:-no}"
  echo -e "  ${BOLD}Listmonk sync:${NC}   ${LISTMONK_SYNC:-no}"
  echo -e "  ${BOLD}Rocket.Chat:${NC}     ${CHAT_ENABLED:-no}"
  echo -e "  ${BOLD}Gancio sync:${NC}     ${GANCIO_SYNC:-no}"
  echo -e "  ${BOLD}Jitsi Meet:${NC}      ${MEET_ENABLED:-no}"
  echo -e "  ${BOLD}SMS Campaigns:${NC}   ${SMS_ENABLED:-no}"
  echo -e "  ${BOLD}Monitoring:${NC}      ${MONITORING_ENABLED:-no}"
  echo -e "  ${BOLD}Docs Comments:${NC}   ${DOCS_COMMENTS_ENABLED:-no}"
  echo -e "  ${BOLD}Bunker Ops:${NC}      ${BUNKER_OPS_ENABLED:-no}"
  echo -e "  ${BOLD}Pangolin:${NC}        ${PANGOLIN_CONFIGURED:-no}"
  echo -e "  ${BOLD}Upgrade watcher:${NC} ${UPGRADE_WATCHER:-skipped}"
  echo -e "  ${BOLD}Backup timer:${NC}    ${BACKUP_TIMER:-skipped}"
  echo -e "  ${BOLD}Secrets:${NC}         22 auto-generated"
  echo ""
  echo -e "  ${DIM}Config file: $ENV_FILE${NC}"
}

print_next_steps() {
  echo ""
  echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}"
  echo -e "${BOLD}${BLUE}  Next Steps${NC}"
  echo -e "${BOLD}${BLUE}══════════════════════════════════════${NC}"
  echo ""

  if [[ "$INSTALL_MODE" == "release" ]]; then
    # Release mode: simpler instructions (production images, auto-migration)
    echo -e "  ${BOLD}1.${NC} Start all services:"
    echo -e "     ${CYAN}docker compose up -d${NC}"
    echo ""
    echo -e "     First run pulls ~40 images (~3 min) and stabilizes health in ~90s."
    echo -e "     Brief unhealthy statuses during this window are expected."
    echo -e "     Database migrations and seeding run automatically on startup."
    echo ""
    echo -e "  ${BOLD}2.${NC} Verify the install:"
    echo -e "     ${CYAN}bash scripts/test-deployment.sh --wait 60${NC}"
    echo ""
    echo -e "     Checks all containers healthy, API responding, (if domain set) tunnel reachable."
    echo ""
    echo -e "  ${BOLD}3.${NC} Access the application:"
    echo -e "     Admin GUI:  ${CYAN}http://localhost:3000${NC}"
    echo -e "     API:        ${CYAN}http://localhost:4000${NC}"
    echo ""
    echo -e "  ${BOLD}4.${NC} Useful tools:"
    echo -e "     ${CYAN}bash scripts/validate-env.sh${NC}        # re-check .env + host ports"
    echo -e "     ${CYAN}bash scripts/pangolin-teardown.sh${NC}   # wipe tunnel org before reinstall (dry-run by default)"
    echo -e "     ${CYAN}docker compose ps${NC}                    # live status"
    echo -e "     ${CYAN}docker compose logs -f api --tail 20${NC}"
    echo ""
  else
    # Source mode: existing instructions
    echo -e "  ${BOLD}1.${NC} Start core services:"
    echo -e "     ${CYAN}docker compose up -d v2-postgres redis api admin${NC}"
    echo ""
    echo -e "  ${BOLD}2.${NC} Run database setup:"
    echo -e "     ${CYAN}docker compose exec api npx prisma migrate deploy${NC}"
    echo -e "     ${CYAN}docker compose exec api npx prisma db seed${NC}"
    echo ""
    echo -e "  ${BOLD}3.${NC} Access the application:"
    echo -e "     Admin GUI:  ${CYAN}http://localhost:3000${NC}"
    echo -e "     API:        ${CYAN}http://localhost:4000${NC}"
    echo ""
    echo -e "  ${BOLD}4.${NC} Optional — start additional services:"
    echo -e "     ${CYAN}docker compose up -d nginx${NC}              # Reverse proxy"
    echo -e "     ${CYAN}docker compose up -d media-api${NC}          # Video library"
    echo -e "     ${CYAN}docker compose up -d listmonk-app${NC}       # Newsletters"
    echo -e "     ${CYAN}docker compose up -d rocketchat${NC}          # Team chat"
    echo -e "     ${CYAN}docker compose up -d jitsi-web jitsi-prosody jitsi-jicofo jitsi-jvb${NC}  # Video calls"
    echo -e "     ${CYAN}docker compose up -d homepage${NC}            # Service dashboard"
    echo ""
    echo -e "  ${BOLD}5.${NC} Or start everything at once (monitoring included if enabled above):"
    echo -e "     ${CYAN}docker compose up -d${NC}"
    echo ""
  fi

  echo -e "  ${YELLOW}IMPORTANT: Change your admin password after first login!${NC}"
  echo -e "  ${YELLOW}JITSI: Ensure UDP port 10000 is open in your firewall for video/audio.${NC}"
  echo ""
}

# =============================================================================
# Main
# =============================================================================

main() {
  print_banner
  check_prerequisites
  initialize_env

  configure_domain
  configure_admin
  generate_all_secrets
  configure_smtp
  configure_features
  configure_pangolin
  configure_control_panel
  configure_cors
  generate_nginx_configs
  generate_services_yaml
  configure_docs_reset
  fix_container_permissions
  install_upgrade_watcher
  install_backup_timer

  # Release mode: auto-set production defaults
  if [[ "$INSTALL_MODE" == "release" ]]; then
    header "Release Mode Settings"
    update_env_var "IMAGE_TAG" "latest"
    update_env_var "NODE_ENV" "production"
    # Ensure monitoring is included if user opted in (preserve existing profiles)
    if [[ "${MONITORING_ENABLED:-no}" == "yes" ]]; then
      local existing_profiles
      existing_profiles=$(grep -oP 'COMPOSE_PROFILES=\K.*' "$ENV_FILE" 2>/dev/null || echo "")
      if [[ -z "$existing_profiles" ]]; then
        update_env_var "COMPOSE_PROFILES" "monitoring"
      elif [[ "$existing_profiles" != *"monitoring"* ]]; then
        update_env_var "COMPOSE_PROFILES" "${existing_profiles},monitoring"
      fi
    fi
    success "Set IMAGE_TAG=latest, NODE_ENV=production (pre-built images)"
  fi

  print_summary
  print_next_steps
}

main "$@"
