#!/usr/bin/env bash
# Hetzner Dispatcher Installer
# Run with: curl -fsSL https://everlyios.com/i | sudo bash
#
# Idempotent: re-runs safely. Detects existing state and only changes what's needed.
# Self-verifying: tests the live endpoint before declaring success.
# Caddy auto-edit is best-effort; dispatcher itself always finishes installing.

set -uo pipefail
# Note: NOT using -e — we handle errors per-step so a Caddy mishap doesn't
# abort the whole install, and we always print a summary at the end.

# ───── styling ─────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
    B=$'\033[1m'; G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; D=$'\033[2m'; N=$'\033[0m'
else
    B=""; G=""; Y=""; R=""; D=""; N=""
fi
say()  { printf "%s\n" "$*"; }
ok()   { printf "${G}✓${N} %s\n" "$*"; }
info() { printf "${B}→${N} %s\n" "$*"; }
warn() { printf "${Y}!${N} %s\n" "$*"; }
err()  { printf "${R}✗${N} %s\n" "$*" >&2; }
die()  { err "$*"; print_summary "FAILED"; exit 1; }

# Summary tracker
declare -a STEPS_OK=()
declare -a STEPS_WARN=()
declare -a STEPS_FAIL=()

step_ok()   { STEPS_OK+=("$1"); }
step_warn() { STEPS_WARN+=("$1"); }
step_fail() { STEPS_FAIL+=("$1"); }

print_summary() {
    local status="${1:-COMPLETE}"
    echo
    say "${B}════════════════════════════════════════════════════════════════${N}"
    say "${B}  Summary [$status]${N}"
    say "${B}════════════════════════════════════════════════════════════════${N}"
    for s in "${STEPS_OK[@]}";   do printf "  ${G}✓${N} %s\n" "$s"; done
    for s in "${STEPS_WARN[@]}"; do printf "  ${Y}!${N} %s\n" "$s"; done
    for s in "${STEPS_FAIL[@]}"; do printf "  ${R}✗${N} %s\n" "$s"; done
    echo
}

# ───── preflight ───────────────────────────────────────────────────────────
[[ $EUID -eq 0 ]] || die "Run as root. Try: curl -fsSL https://everlyios.com/i | sudo bash"
command -v curl    >/dev/null || die "curl is required"
command -v openssl >/dev/null || die "openssl is required"
command -v python3 >/dev/null || die "python3 is required"

DISPATCH_DOMAIN="${DISPATCH_DOMAIN:-cards.everlygroup.org}"
DISPATCH_PATH="${DISPATCH_PATH:-/dispatch}"
DISPATCH_PORT="${DISPATCH_PORT:-8765}"
DISPATCH_USER="${DISPATCH_USER:-dispatch}"
DISPATCH_HOME="/opt/dispatch"
ENV_FILE="/etc/dispatch.env"
SUDOERS_FILE="/etc/sudoers.d/dispatch"
INSTALL_BASE="${INSTALL_BASE:-https://everlyios.com/i}"

echo
say "${B}════════════════════════════════════════════════════════════════${N}"
say "${B}  Hetzner Dispatcher Installer${N}"
say "${B}════════════════════════════════════════════════════════════════${N}"
say "  Target:  ${DISPATCH_DOMAIN}${DISPATCH_PATH}"
say "  Port:    localhost:${DISPATCH_PORT}"
say "  User:    ${DISPATCH_USER}"
say "  Source:  ${INSTALL_BASE}"
echo

# ───── 1. system deps ──────────────────────────────────────────────────────
info "[1/8] System dependencies"
export DEBIAN_FRONTEND=noninteractive
if apt-get update -qq 2>/dev/null && \
   apt-get install -y -qq python3 python3-venv python3-pip jq ca-certificates >/dev/null 2>&1; then
    ok "Installed: python3, venv, pip, jq, ca-certificates"
    step_ok "System dependencies"
else
    warn "apt-get had issues; checking if we have what we need anyway"
    if command -v python3 >/dev/null && python3 -c "import venv" 2>/dev/null; then
        ok "Required packages already present"
        step_ok "System dependencies (pre-existing)"
    else
        die "Could not install required packages"
    fi
fi
echo

# ───── 2. user + dirs ──────────────────────────────────────────────────────
info "[2/8] User and directories"
if ! id "$DISPATCH_USER" &>/dev/null; then
    useradd --system --home-dir "$DISPATCH_HOME" --create-home --shell /usr/sbin/nologin "$DISPATCH_USER"
    ok "Created user '$DISPATCH_USER'"
else
    ok "User '$DISPATCH_USER' already exists"
fi
mkdir -p "$DISPATCH_HOME" /var/lib/dispatch/jobs /var/log/dispatch
chown -R "$DISPATCH_USER:$DISPATCH_USER" "$DISPATCH_HOME" /var/lib/dispatch /var/log/dispatch
ok "Directories: $DISPATCH_HOME, /var/lib/dispatch, /var/log/dispatch"
step_ok "User and directories"
echo

# ───── 3. virtualenv + app ─────────────────────────────────────────────────
info "[3/8] Python virtualenv and app"
if [[ ! -d "$DISPATCH_HOME/venv" ]]; then
    if sudo -u "$DISPATCH_USER" python3 -m venv "$DISPATCH_HOME/venv"; then
        ok "venv created"
    else
        die "venv creation failed (try: apt install python3-venv)"
    fi
fi
sudo -u "$DISPATCH_USER" "$DISPATCH_HOME/venv/bin/pip" install --quiet --upgrade pip wheel >/dev/null
if sudo -u "$DISPATCH_USER" "$DISPATCH_HOME/venv/bin/pip" install --quiet "fastapi>=0.100" "uvicorn[standard]>=0.20" >/dev/null; then
    ok "fastapi + uvicorn installed"
else
    die "pip install failed"
fi

APP_URL="${INSTALL_BASE}/dispatch_app.py"
if curl -fsSL "$APP_URL" -o "$DISPATCH_HOME/dispatch_app.py.new"; then
    mv "$DISPATCH_HOME/dispatch_app.py.new" "$DISPATCH_HOME/dispatch_app.py"
    chown "$DISPATCH_USER:$DISPATCH_USER" "$DISPATCH_HOME/dispatch_app.py"
    chmod 644 "$DISPATCH_HOME/dispatch_app.py"
    ok "App installed"
else
    die "Could not fetch dispatch_app.py from $APP_URL"
fi
step_ok "Dispatcher app"
echo

# ───── 4. token + env file ─────────────────────────────────────────────────
info "[4/8] Authentication token"
if [[ -f "$ENV_FILE" ]] && grep -q "^DISPATCH_TOKEN=" "$ENV_FILE"; then
    TOKEN=$(grep "^DISPATCH_TOKEN=" "$ENV_FILE" | cut -d= -f2-)
    ok "Reused existing token"
else
    TOKEN=$(openssl rand -hex 32)
    cat > "$ENV_FILE" <<EOF
DISPATCH_TOKEN=$TOKEN
EOF
    chmod 600 "$ENV_FILE"
    chown root:root "$ENV_FILE"
    ok "New token generated"
fi
step_ok "Token"
echo

# ───── 5. sudoers ──────────────────────────────────────────────────────────
info "[5/8] Sudoers"
cat > "${SUDOERS_FILE}.new" <<EOF
# Generated by dispatch installer — do not edit by hand
$DISPATCH_USER ALL=(root) NOPASSWD: /usr/bin/docker, /usr/bin/docker-compose, /usr/local/bin/docker-compose, /usr/bin/systemctl, /usr/local/bin/caddy, /usr/sbin/caddy, /usr/bin/journalctl
EOF
chmod 440 "${SUDOERS_FILE}.new"
if visudo -cf "${SUDOERS_FILE}.new" >/dev/null 2>&1; then
    mv "${SUDOERS_FILE}.new" "$SUDOERS_FILE"
    ok "Sudoers entry installed"
    step_ok "Sudoers"
else
    rm -f "${SUDOERS_FILE}.new"
    warn "Sudoers validation failed; skipping"
    step_warn "Sudoers (validation failed)"
fi
echo

# ───── 6. systemd unit ─────────────────────────────────────────────────────
info "[6/8] Systemd service"
cat > /etc/systemd/system/dispatch.service <<EOF
[Unit]
Description=Hetzner Dispatcher (HTTPS shell)
After=network.target
Wants=network.target

[Service]
Type=simple
User=$DISPATCH_USER
Group=$DISPATCH_USER
WorkingDirectory=$DISPATCH_HOME
EnvironmentFile=$ENV_FILE
ExecStart=$DISPATCH_HOME/venv/bin/python3 $DISPATCH_HOME/dispatch_app.py
Restart=always
RestartSec=3
StandardOutput=append:/var/log/dispatch/dispatch.out
StandardError=append:/var/log/dispatch/dispatch.err
NoNewPrivileges=false
PrivateTmp=true
ProtectHome=true
ReadWritePaths=/var/lib/dispatch /var/log/dispatch /opt/dispatch

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable dispatch.service >/dev/null 2>&1
systemctl restart dispatch.service
sleep 3

if systemctl is-active --quiet dispatch.service; then
    ok "dispatch.service running"
    step_ok "Systemd service"
else
    warn "Service not active. Recent logs:"
    journalctl -u dispatch.service -n 20 --no-pager || true
    die "dispatch.service failed to start"
fi

LOCAL_OK=0
for i in 1 2 3 4 5; do
    if curl -fsS "http://127.0.0.1:${DISPATCH_PORT}/health" 2>/dev/null | grep -q '"ok":true'; then
        LOCAL_OK=1
        break
    fi
    sleep 1
done
if [[ $LOCAL_OK -eq 1 ]]; then
    ok "Localhost dispatcher responding on :$DISPATCH_PORT"
    step_ok "Localhost health"
else
    die "Localhost dispatcher not responding on port $DISPATCH_PORT"
fi
echo

# ───── 7. Caddy route ──────────────────────────────────────────────────────
info "[7/8] Caddy reverse proxy"

CADDY_MODE=""
CADDY_CONTAINER=""
if systemctl list-unit-files 2>/dev/null | grep -q "^caddy.service" && \
   systemctl is-active --quiet caddy 2>/dev/null; then
    CADDY_MODE="systemd"
    ok "Caddy detected as systemd service"
elif command -v docker >/dev/null 2>&1; then
    CADDY_CONTAINER=$(docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null | grep -i caddy | head -1 | awk '{print $1}' || true)
    if [[ -n "$CADDY_CONTAINER" ]]; then
        CADDY_MODE="docker"
        ok "Caddy detected as Docker container: $CADDY_CONTAINER"
    fi
fi

CADDY_DONE=0
if [[ -z "$CADDY_MODE" ]]; then
    warn "Could not detect a running Caddy instance"
    step_warn "Caddy not detected — manual setup needed"
else
    CANDIDATES=(
        /etc/caddy/Caddyfile
        /opt/n8n/Caddyfile
        /opt/n8n/caddy/Caddyfile
        /opt/caddy/Caddyfile
        /opt/caddy/conf/Caddyfile
        /root/Caddyfile
    )
    if [[ "$CADDY_MODE" == "docker" ]]; then
        # Add Caddyfile mounts from container inspect
        MOUNT_SRCS=$(docker inspect "$CADDY_CONTAINER" 2>/dev/null | python3 -c "
import json,sys
try:
    data = json.load(sys.stdin)
    for m in data[0].get('Mounts', []):
        src = m.get('Source','')
        dst = m.get('Destination','')
        if src.endswith('Caddyfile'):
            print(src)
        elif dst.endswith('Caddyfile'):
            print(src)
except Exception:
    pass
" 2>/dev/null)
        for m in $MOUNT_SRCS; do
            [[ -f "$m" ]] && CANDIDATES=("$m" "${CANDIDATES[@]}")
        done
    fi

    CADDYFILE=""
    for candidate in "${CANDIDATES[@]}"; do
        if [[ -f "$candidate" ]] && grep -q "cards.everlygroup.org" "$candidate" 2>/dev/null; then
            CADDYFILE="$candidate"
            break
        fi
    done

    if [[ -z "$CADDYFILE" ]]; then
        warn "Could not locate Caddyfile containing 'cards.everlygroup.org'"
        warn ""
        warn "To finish manually, add this inside the cards.everlygroup.org { } block:"
        cat <<EOF

    handle_path /dispatch/* {
        reverse_proxy host.docker.internal:$DISPATCH_PORT
    }

EOF
        step_warn "Caddyfile not found"
    else
        ok "Caddyfile found: $CADDYFILE"
        BACKUP="${CADDYFILE}.bak.$(date +%Y%m%d-%H%M%S)"
        cp "$CADDYFILE" "$BACKUP"
        ok "Backup: $BACKUP"

        # Pick the upstream address
        UPSTREAM_HOST="127.0.0.1"
        if [[ "$CADDY_MODE" == "docker" ]]; then
            HAS_HOST_GW=$(docker inspect "$CADDY_CONTAINER" --format '{{json .HostConfig.ExtraHosts}}' 2>/dev/null | grep -c "host-gateway" || true)
            if [[ "${HAS_HOST_GW:-0}" -gt 0 ]]; then
                UPSTREAM_HOST="host.docker.internal"
                ok "Container has host-gateway → host.docker.internal"
            else
                BRIDGE_IP=$(ip -4 addr show docker0 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1)
                if [[ -n "$BRIDGE_IP" ]]; then
                    UPSTREAM_HOST="$BRIDGE_IP"
                    ok "Using docker0 bridge: $BRIDGE_IP"
                else
                    UPSTREAM_HOST="172.17.0.1"
                    warn "No bridge IP found; defaulting to 172.17.0.1"
                fi
            fi
        fi
        UPSTREAM="$UPSTREAM_HOST:$DISPATCH_PORT"

        # Check if already configured
        CADDY_EDITED=0
        if grep -q "/dispatch/" "$CADDYFILE" && grep -q ":$DISPATCH_PORT" "$CADDYFILE"; then
            ok "Dispatch route already present"
            CADDY_EDITED=1
        else
            EDIT_RESULT=$(UPSTREAM="$UPSTREAM" CADDYFILE="$CADDYFILE" python3 <<'PYEOF'
import re, sys, os
path = os.environ["CADDYFILE"]
upstream = os.environ["UPSTREAM"]
try:
    with open(path) as f:
        text = f.read()
    text = re.sub(r'\n\s*# === dispatcher.*?# === end dispatcher ===\n?', '\n', text, flags=re.DOTALL)
    pattern = re.compile(r'(\bcards\.everlygroup\.org\b[^{]*\{)', re.MULTILINE)
    m = pattern.search(text)
    if not m:
        print("NOMATCH"); sys.exit(0)
    insert_at = m.end()
    block = f"""
    # === dispatcher (added by installer) ===
    handle_path /dispatch/* {{
        reverse_proxy {upstream}
    }}
    # === end dispatcher ==="""
    new_text = text[:insert_at] + block + text[insert_at:]
    with open(path, "w") as f:
        f.write(new_text)
    print("OK")
except Exception as e:
    print(f"ERR:{e}")
PYEOF
)
            case "$EDIT_RESULT" in
                OK)
                    ok "Route inserted (upstream: $UPSTREAM)"
                    CADDY_EDITED=1
                    ;;
                NOMATCH)
                    warn "cards.everlygroup.org block not found in $CADDYFILE"
                    cp "$BACKUP" "$CADDYFILE"
                    CADDY_EDITED=-1
                    ;;
                *)
                    warn "Edit failed: $EDIT_RESULT"
                    cp "$BACKUP" "$CADDYFILE"
                    CADDY_EDITED=-1
                    ;;
            esac
        fi

        # Reload
        RELOAD_OK=0
        if [[ $CADDY_EDITED -ge 0 ]]; then
            if [[ "$CADDY_MODE" == "systemd" ]]; then
                if command -v caddy >/dev/null && caddy validate --config "$CADDYFILE" --adapter caddyfile >/tmp/caddy_validate.log 2>&1; then
                    systemctl reload caddy 2>/tmp/caddy_reload.log && RELOAD_OK=1 || true
                else
                    # No caddy binary or validate failed; try reload anyway
                    systemctl reload caddy 2>/tmp/caddy_reload.log && RELOAD_OK=1 || true
                fi
                [[ $RELOAD_OK -eq 0 ]] && cp "$BACKUP" "$CADDYFILE"
            else
                # Docker. Use docker exec to reload via Caddy's API/CLI
                if docker exec "$CADDY_CONTAINER" which caddy >/dev/null 2>&1; then
                    # Find inner path
                    INNER_PATH=$(docker inspect "$CADDY_CONTAINER" 2>/dev/null | python3 -c "
import json,sys,os
host=os.environ.get('HOST_PATH','')
try:
    data=json.load(sys.stdin)
    for m in data[0].get('Mounts',[]):
        if m.get('Source')==host:
            print(m.get('Destination')); break
except Exception: pass
" 2>/dev/null HOST_PATH="$CADDYFILE")
                    INNER_PATH=${INNER_PATH:-/etc/caddy/Caddyfile}
                    if docker exec "$CADDY_CONTAINER" caddy validate --config "$INNER_PATH" --adapter caddyfile >/tmp/caddy_validate.log 2>&1; then
                        docker exec "$CADDY_CONTAINER" caddy reload --config "$INNER_PATH" --adapter caddyfile >/tmp/caddy_reload.log 2>&1 && RELOAD_OK=1
                    fi
                    [[ $RELOAD_OK -eq 0 ]] && {
                        warn "Caddy validate/reload failed; rolling back"
                        head -20 /tmp/caddy_validate.log /tmp/caddy_reload.log 2>/dev/null | head -20
                        cp "$BACKUP" "$CADDYFILE"
                    }
                else
                    warn "No caddy binary in container; trying container restart"
                    if docker restart "$CADDY_CONTAINER" >/dev/null 2>&1; then
                        sleep 4
                        RELOAD_OK=1
                    fi
                fi
            fi

            if [[ $RELOAD_OK -eq 1 ]]; then
                ok "Caddy reloaded"
                step_ok "Caddy reverse proxy"
                CADDY_DONE=1
            else
                warn "Caddy reload failed — see /tmp/caddy_validate.log and /tmp/caddy_reload.log"
                step_warn "Caddy reload — manual review needed"
            fi
        fi
    fi
fi
echo

# ───── 8. end-to-end self-test ─────────────────────────────────────────────
info "[8/8] End-to-end verification"
PUBLIC_URL="https://${DISPATCH_DOMAIN}${DISPATCH_PATH}"
say "  Endpoint: $PUBLIC_URL"
sleep 3

PUBLIC_OK=0
PUBLIC_HEALTH=$(curl -fsS -m 10 "$PUBLIC_URL/health" 2>&1 || echo "FAIL")
if echo "$PUBLIC_HEALTH" | grep -q '"ok":true'; then
    ok "Public /health responding"
    PUBLIC_OK=1
fi

AUTH_OK=0
AUTH_TEST=$(curl -fsS -m 15 -X POST -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"cmd":"echo dispatcher-online && hostname && uptime -p"}' \
    "$PUBLIC_URL/run" 2>&1 || echo "FAIL")
if echo "$AUTH_TEST" | grep -q "dispatcher-online"; then
    ok "Authenticated /run working"
    AUTH_OK=1
fi

if [[ $PUBLIC_OK -eq 1 && $AUTH_OK -eq 1 ]]; then
    step_ok "End-to-end verified"
else
    [[ $PUBLIC_OK -eq 0 ]] && warn "Public health: $(echo "$PUBLIC_HEALTH" | head -c 200)"
    [[ $AUTH_OK -eq 0 && $PUBLIC_OK -eq 1 ]] && warn "/run failed: $(echo "$AUTH_TEST" | head -c 200)"
    step_warn "Public endpoint not fully verified"
fi
echo

print_summary

say "  ${B}Service:${N}     systemctl status dispatch"
say "  ${B}Live logs:${N}   journalctl -u dispatch -f"
say "  ${B}App logs:${N}    tail -f /var/log/dispatch/dispatch.log"
say "  ${B}Token file:${N}  $ENV_FILE ${D}(root-only readable)${N}"
echo
if [[ ${PUBLIC_OK:-0} -eq 1 && ${AUTH_OK:-0} -eq 1 ]]; then
    say "  ${G}${B}✓ LIVE${N}  $PUBLIC_URL/health"
    say "  ${G}${B}✓ Authenticated /run works${N}"
    echo
    say "  ${D}Claude will save the token to Supabase Vault as${N}"
    say "  ${D}HETZNER_DISPATCH_TOKEN. No further action needed.${N}"
else
    say "  ${Y}${B}Partial install${N} — dispatcher is running locally."
    say "  ${Y}Public Caddy route needs manual attention.${N}"
    say "  ${D}Local works:  curl http://127.0.0.1:${DISPATCH_PORT}/health${N}"
fi
echo
echo "DISPATCH_TOKEN_OUTPUT_BEGIN"
echo "DISPATCH_TOKEN=$TOKEN"
echo "DISPATCH_TOKEN_OUTPUT_END"
echo
