#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>

# SecuBox-Deb :: secubox-aggregator-migrate
# Phase 7 (#496) — automate cutover from per-module uvicorn to aggregator.
#
# Steps :
#   1. Discover all /usr/lib/secubox/<name>/api/main.py modules
#   2. Generate /etc/secubox/aggregator.toml with the full list
#   3. Restart aggregator and capture which modules mounted successfully
#   4. For each mounted module :
#        - Switch nginx proxy_pass to /run/secubox/aggregator.sock
#        - Stop + disable secubox-<name>.service
#   5. Reload nginx
#   6. Report savings
#
# Idempotent. Operator can re-run after package upgrades that add modules.

set -euo pipefail
readonly MODULE="secubox-aggregator-migrate"
readonly VERSION="0.1.0"

log() { printf '[%s] %s\n' "$MODULE" "$*" >&2; }
err() { printf '[%s] ERROR: %s\n' "$MODULE" "$*" >&2; exit 1; }

[ -d /usr/lib/secubox ] || err "no /usr/lib/secubox — SecuBox modules not installed"
[ -d /run/secubox ] || err "no /run/secubox runtime dir — secubox-core required"
command -v curl >/dev/null || err "curl required"

log "Phase 7 ASGI migration — v${VERSION}"

# ── Step 1+2 : Discover modules + write aggregator.toml ──────────────────
#
# Older packaging variants installed under /usr/lib/secubox/secubox-<name>/
# (with the package-name prefix) before being superseded by the canonical
# unprefixed /usr/lib/secubox/<name>/. Both directories often coexist on
# upgraded systems. We filter out the `secubox-*` prefixed siblings here :
# their FastAPIs are byte-identical with the unprefixed ones, so mounting
# both would just waste memory and create ghost openapi.json entries.
TOML_TMP=$(mktemp)
{
    echo "# Phase 7 (#496) — auto-generated by ${MODULE}"
    echo "# Run secubox-aggregator-migrate to refresh after installing new modules."
    echo "modules = ["
    for f in /usr/lib/secubox/*/api/main.py; do
        [ -f "$f" ] || continue
        m=$(basename "$(dirname "$(dirname "$f")")")
        case "$m" in
            secubox-*)
                # legacy prefixed dir — canonical unprefixed twin wins
                stripped="${m#secubox-}"
                if [ -f "/usr/lib/secubox/${stripped}/api/main.py" ]; then
                    log "skip ${m} (duplicate of ${stripped})"
                    continue
                fi
                ;;
        esac
        printf '    "%s",\n' "$m"
    done
    echo "]"
} > "$TOML_TMP"

DISCOVERED=$(grep -c '^    "' "$TOML_TMP" || echo 0)
log "discovered ${DISCOVERED} modules"

install -d -m 0755 /etc/secubox
install -m 0644 "$TOML_TMP" /etc/secubox/aggregator.toml
rm -f "$TOML_TMP"

# ── Step 3 : Restart aggregator + wait for mount completion ──────────────
systemctl restart secubox-aggregator
sleep 8

# ── Step 4 : Grab mounted module list from /health ───────────────────────
HEALTH=$(curl -s --unix-socket /run/secubox/aggregator.sock http://localhost/health 2>&1) || \
    err "aggregator /health unreachable"

MOUNTED=$(printf '%s' "$HEALTH" | python3 -c "
import sys, json
try:
    d = json.loads(sys.stdin.read())
    print(' '.join(d.get('mounted', [])))
except Exception as e:
    print('', end='')
    sys.stderr.write(f'parse: {e}\n')
")

if [ -z "$MOUNTED" ]; then
    err "no modules mounted in aggregator — check 'journalctl -u secubox-aggregator'"
fi

MOUNTED_COUNT=$(printf '%s' "$MOUNTED" | wc -w)
log "aggregator mounted ${MOUNTED_COUNT} modules — proceeding with cutover"

# ── Step 5 : nginx proxy_pass switch ─────────────────────────────────────
#
# Per-module FastAPIs used to listen on /run/secubox/<name>.sock and the
# per-module nginx route did :
#     proxy_pass http://unix:.../<name>.sock:/;
# The trailing `:/` strips the matching location prefix, so the upstream
# saw a bare `/foo` URI and matched its top-level routes.
#
# The aggregator mounts each sub-app under /api/v1/<name>, so it expects
# the FULL prefix to arrive intact. Rewrite must :
#   1. Point at /run/secubox/aggregator.sock
#   2. Preserve the /api/v1/<name>/ prefix on the upstream URI
#
# Three locations need patching :
#   /etc/nginx/secubox.d/<name>.conf         ← legacy module-side drop-ins
#   /etc/nginx/secubox-routes.d/<name>.conf  ← newer per-module route files
#   /etc/nginx/sites-enabled/webui.conf      ← inline per-module location blocks
#                                              + the /api/ catch-all fallback
# Plus one special case : hub used to listen on TCP 127.0.0.1:8001.
SWITCHED=0
NGINX_SKIPPED=0

_rewrite_file() {
    local f="$1" name="$2"
    [ -f "$f" ] || return 0
    case "$f" in *.bak*|*.dpkg*|*.disabled*) return 0 ;; esac
    # First-run backup
    [ -f "${f}.bak.phase7" ] || cp "$f" "${f}.bak.phase7"
    # Every per-module unix socket → aggregator + /api/v1/<name>/ prefix
    sed -i -E \
      -e "s|proxy_pass http://unix:/run/secubox/${name}\.sock(:/[^;]*)?;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/${name}/;|g" \
      -e "s|proxy_pass http://unix:/run/secubox/aggregator\.sock:/;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/${name}/;|g" \
      "$f"
}

for name in $MOUNTED; do
    found_any=0
    for dir in /etc/nginx/secubox.d /etc/nginx/secubox-routes.d; do
        cfg="${dir}/${name}.conf"
        if [ -f "$cfg" ]; then
            _rewrite_file "$cfg" "$name"
            found_any=1
        fi
    done
    if [ "$found_any" -eq 0 ]; then
        NGINX_SKIPPED=$((NGINX_SKIPPED + 1))
        continue
    fi
    SWITCHED=$((SWITCHED + 1))
done

# Hub special case : it used TCP 127.0.0.1:8001 (not a unix socket). Replace
# every such line in secubox.d/hub.conf and in webui.conf inline blocks.
HUB_CFG=/etc/nginx/secubox.d/hub.conf
if [ -f "$HUB_CFG" ]; then
    [ -f "${HUB_CFG}.bak.phase7" ] || cp "$HUB_CFG" "${HUB_CFG}.bak.phase7"
    sed -i -E "s|proxy_pass http://127\.0\.0\.1:8001/?;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/hub/;|g" "$HUB_CFG"
fi

# webui.conf : inline `location /api/v1/<name>/` blocks + `/api/` catch-all
WEBUI=/etc/nginx/sites-enabled/webui.conf
if [ -f "$WEBUI" ]; then
    [ -f "/root/webui.conf.bak.phase7" ] || cp "$WEBUI" /root/webui.conf.bak.phase7
    # NOTE: backup stored under /root, not next to $WEBUI, because nginx
    # auto-includes anything matching sites-enabled/* and would crash on
    # duplicate-default-server.
    for name in $MOUNTED; do
        sed -i -E \
          -e "s|proxy_pass http://unix:/run/secubox/${name}\.sock(:/[^;]*)?;|proxy_pass http://unix:/run/secubox/aggregator.sock:/api/v1/${name}/;|g" \
          "$WEBUI"
    done
    # Catch-all : keep the full URI by removing any rewrite on /api/
    sed -i -E "/location \/api\/ \{/,/\}/ s|proxy_pass http://127\.0\.0\.1:8001;|proxy_pass http://unix:/run/secubox/aggregator.sock;|" "$WEBUI"
fi

log "nginx routes switched : ${SWITCHED} (skipped ${NGINX_SKIPPED} with no route file)"

# Validate + reload nginx
if ! nginx -t 2>/dev/null; then
    err "nginx config check failed — review /etc/nginx/{secubox.d,secubox-routes.d,sites-enabled/webui.conf}"
fi
systemctl reload nginx
log "nginx reloaded"

# ── Step 6 : Disable migrated per-module systemd units ───────────────────
STOPPED=0
for name in $MOUNTED; do
    svc=secubox-${name}.service
    systemctl is-active "$svc" >/dev/null 2>&1 || continue
    systemctl stop "$svc" 2>/dev/null || true
    systemctl disable "$svc" 2>/dev/null || true
    STOPPED=$((STOPPED + 1))
done
log "stopped + disabled : ${STOPPED} per-module services"

# ── Step 7 : Final report ───────────────────────────────────────────────
cat <<REPORT

────────────────────────────────────────────────────────────────────
Phase 7 ASGI migration complete

  Modules discovered           : ${DISCOVERED}
  Modules mounted (aggregator) : ${MOUNTED_COUNT}
  Modules failed to mount      : $((DISCOVERED - MOUNTED_COUNT))
  Nginx routes switched        : ${SWITCHED}
  Per-module services stopped  : ${STOPPED}

Tip : a reboot is recommended to recover glibc memory fragmentation
left by the previous per-module processes :

    sudo systemctl reboot

Failed mount reasons (relative imports etc.) :

    curl --unix-socket /run/secubox/aggregator.sock \\
         http://localhost/health | python3 -m json.tool

Rollback per module if needed :

    # restore whichever of these exist
    cp /etc/nginx/secubox.d/<name>.conf.bak.phase7 \\
       /etc/nginx/secubox.d/<name>.conf 2>/dev/null
    cp /etc/nginx/secubox-routes.d/<name>.conf.bak.phase7 \\
       /etc/nginx/secubox-routes.d/<name>.conf 2>/dev/null
    cp /root/webui.conf.bak.phase7 /etc/nginx/sites-enabled/webui.conf
    systemctl enable --now secubox-<name>.service
    systemctl reload nginx
────────────────────────────────────────────────────────────────────
REPORT
