#!/bin/bash
# SHIFTDECK Host-Updater (v6.67+)
# ==================================================================
# Dieses Skript läuft AUSSERHALB des Backend-Containers auf dem Host.
# Es wird NICHT vom Backend generiert, sondern liegt als echte Datei
# in /opt/shiftdeck/bin/shiftdeck-update und kann jederzeit manuell
# aufgerufen werden:
#
#   bash /opt/shiftdeck/bin/shiftdeck-update 6.67
#
# Das Backend ruft es einfach auf — die Logik ist aber hier drin.
# Selbst wenn das Backend buggy ist, funktioniert das Update.
#
# Pfad:   /opt/shiftdeck/bin/shiftdeck-update
# Status: /opt/shiftdeck/updates/status.json
# Log:    /opt/shiftdeck/updates/update.log
# ==================================================================

set -u
set -o pipefail

INSTALL_DIR="${INSTALL_DIR:-/opt/shiftdeck}"
COMPOSE_FILE="$INSTALL_DIR/docker-compose.prod.yml"
UPDATE_DIR="$INSTALL_DIR/updates"
STATUS_FILE="$UPDATE_DIR/status.json"
LOG_FILE="$UPDATE_DIR/update.log"
BACKUP_DIR="$INSTALL_DIR/backups"
DOWNLOAD_BASE="${DOWNLOAD_BASE:-https://shiftdeck.de/download/update}"

TARGET_VERSION="${1:-}"
if [ -z "$TARGET_VERSION" ]; then
    echo "Usage: $0 <target_version>" >&2
    exit 2
fi

CURRENT_VERSION="unknown"
[ -f "$INSTALL_DIR/VERSION" ] && CURRENT_VERSION=$(tr -d '[:space:]' < "$INSTALL_DIR/VERSION")

STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)

mkdir -p "$UPDATE_DIR" "$BACKUP_DIR"
exec > >(tee -a "$LOG_FILE") 2>&1

write_status() {
    local stage="$1"; local progress="$2"; local message="$3"; local error="${4:-}"
    cat > "$STATUS_FILE" <<EOF
{
  "stage": "$stage",
  "progress": $progress,
  "message": $(jq -Rs . <<< "$message" 2>/dev/null || echo "\"$message\""),
  "current_version": "$CURRENT_VERSION",
  "target_version": "$TARGET_VERSION",
  "updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "started_at": "$STARTED_AT",
  "error": $(jq -Rs . <<< "$error" 2>/dev/null || echo "\"$error\"")
}
EOF
}

fail() {
    local msg="$1"
    echo "❌ FEHLER: $msg"
    write_status "failed" 0 "$msg" "$msg"
    # Auch bei Fehler: Container müssen laufen, sonst ist Server unerreichbar
    echo "→ Stelle sicher dass Container laufen..."
    docker compose -f "$COMPOSE_FILE" up -d 2>&1 | tail -5 || true
    exit 1
}

echo "========================================================"
echo "SHIFTDECK Update v$CURRENT_VERSION → v$TARGET_VERSION"
echo "Start: $(date)"
echo "Host-Updater v6.67+ (außerhalb Container)"
echo "========================================================"
cd "$INSTALL_DIR" || fail "Install-Verzeichnis nicht gefunden"

# ─── 1. Backup ─────────────────────────────────────────────
write_status "backup" 10 "Backup wird erstellt..."
BACKUP_NAME="backup-v${CURRENT_VERSION}-$(date +%Y%m%d_%H%M%S)"
BACKUP_PATH="$BACKUP_DIR/$BACKUP_NAME"
mkdir -p "$BACKUP_PATH"
for f in VERSION CHANGELOG.md install.sh update.sh docker-compose.prod.yml .env; do
    [ -f "$INSTALL_DIR/$f" ] && cp "$INSTALL_DIR/$f" "$BACKUP_PATH/" 2>/dev/null || true
done
for d in backend frontend updater; do
    [ -d "$INSTALL_DIR/$d" ] && cp -r "$INSTALL_DIR/$d" "$BACKUP_PATH/" 2>/dev/null || true
done
echo "✓ Backup: $BACKUP_PATH"

# ─── 2. Download ───────────────────────────────────────────
write_status "download" 25 "Lade Version $TARGET_VERSION..."
cd "$UPDATE_DIR"
rm -f shiftdeck-core-*.tar.gz shiftdeck-docs-*.tar.gz
CORE_URL="$DOWNLOAD_BASE/shiftdeck-core-$TARGET_VERSION.tar.gz"
DOCS_URL="$DOWNLOAD_BASE/shiftdeck-docs-$TARGET_VERSION.tar.gz"
echo "→ Downloade von: $CORE_URL"
curl -fsSL -o "shiftdeck-core-$TARGET_VERSION.tar.gz" "$CORE_URL" || fail "Core-Download fehlgeschlagen"
curl -fsSL -o "shiftdeck-docs-$TARGET_VERSION.tar.gz" "$DOCS_URL" || echo "⚠ Docs-Download optional, übersprungen"

# Magic-Bytes prüfen: echtes gzip?
if ! file "shiftdeck-core-$TARGET_VERSION.tar.gz" | grep -q "gzip compressed"; then
    head -c 200 "shiftdeck-core-$TARGET_VERSION.tar.gz"
    fail "Download ungültig (keine gzip-Datei) — vermutlich 404 oder HTML"
fi
SIZE=$(stat -c%s "shiftdeck-core-$TARGET_VERSION.tar.gz")
echo "✓ Download OK ($SIZE Bytes)"

# ─── 3. Entpacken ──────────────────────────────────────────
write_status "extract" 40 "Pakete werden entpackt..."
rm -rf "$UPDATE_DIR/shiftdeck"
tar -xzf "shiftdeck-core-$TARGET_VERSION.tar.gz" -C "$UPDATE_DIR" || fail "Entpacken fehlgeschlagen"
[ -f "shiftdeck-docs-$TARGET_VERSION.tar.gz" ] && tar -xzf "shiftdeck-docs-$TARGET_VERSION.tar.gz" -C "$UPDATE_DIR" 2>/dev/null || true
echo "✓ Entpackt"

# ─── 4. Installieren ───────────────────────────────────────
write_status "install" 55 "Neue Dateien werden installiert..."
SRC="$UPDATE_DIR/shiftdeck"
[ -d "$SRC" ] || fail "Entpackte Struktur nicht gefunden"

for f in VERSION CHANGELOG.md; do
    [ -f "$SRC/$f" ] && cp "$SRC/$f" "$INSTALL_DIR/"
done
for d in backend frontend updater; do
    if [ -d "$SRC/$d" ]; then
        rm -rf "$INSTALL_DIR/$d"
        cp -r "$SRC/$d" "$INSTALL_DIR/"
    fi
done
# Docker-compose nur überschreiben wenn vorhanden im Tarball (selten)
[ -f "$SRC/docker-compose.prod.yml" ] && cp "$SRC/docker-compose.prod.yml" "$INSTALL_DIR/"

# bin/shiftdeck-update → sich selbst aktualisieren (für nächstes Update!)
mkdir -p "$INSTALL_DIR/bin"
if [ -f "$SRC/bin/shiftdeck-update" ]; then
    cp "$SRC/bin/shiftdeck-update" "$INSTALL_DIR/bin/shiftdeck-update"
    chmod +x "$INSTALL_DIR/bin/shiftdeck-update"
    echo "✓ Host-Updater selbst aktualisiert"
fi
echo "✓ Dateien installiert"

# ─── 5. Build-Cache managen ────────────────────────────────
write_status "build" 55 "Prüfe Docker-Cache..."
CACHE_RAW=$(docker system df --format '{{.Size}}' 2>/dev/null | grep -oP '\d+(?=GB)' | head -1 || echo "0")
if [ "${CACHE_RAW:-0}" -gt 5 ]; then
    echo "→ Build-Cache ist groß (${CACHE_RAW}GB), räume alte Layer auf..."
    docker builder prune -f --filter "until=168h" 2>&1 | tail -3 || true
fi

# ─── 6. Build ──────────────────────────────────────────────
write_status "build" 60 "Backend + Frontend werden parallel gebaut..."
BUILD_START=$(date +%s)
DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 \
docker compose -f "$COMPOSE_FILE" build --parallel \
    --build-arg SHIFTDECK_VERSION="$TARGET_VERSION" 2>&1 | tail -20 \
    || fail "Build fehlgeschlagen"
BUILD_TIME=$(($(date +%s) - BUILD_START))
echo "✓ Beide Builds abgeschlossen in ${BUILD_TIME}s"

# ─── 7. Container-Restart (robust mit Timeouts) ─────────────────────────
write_status "restart" 85 "Alte Container werden gestoppt (max. 30s)..."
echo "→ Stoppe alte Container (Timeout 15s, danach Hart-Kill)..."

# v6.76: HARTER TIMEOUT statt unendliches Hängen.
# `docker compose stop -t 15` gibt dem Container 15s zum geordneten Stop,
# danach wird SIGKILL gesendet. Ohne -t kann das ewig hängen.
# Plus: timeout 30 als Notbremse falls docker selbst hängt.
timeout 30 docker compose -f "$COMPOSE_FILE" stop -t 15 2>&1 | tail -3 || {
    echo "⚠ stop hat Timeout erreicht — erzwinge Kill..."
    timeout 10 docker kill admin-server-backend admin-server-frontend 2>/dev/null || true
}

# Container entfernen mit Timeout
timeout 20 docker compose -f "$COMPOSE_FILE" rm -f 2>&1 | tail -3 || {
    echo "⚠ rm hat Timeout erreicht — erzwinge Entfernung..."
    timeout 10 docker rm -f admin-server-backend admin-server-frontend 2>/dev/null || true
}

# Paranoid-Cleanup: verwaiste Prefix-Container wegräumen
for c in $(docker ps -a --format '{{.Names}}' | grep -E '_admin-server-(backend|frontend|mongo)$' || true); do
    echo "  → Entferne verwaisten Container: $c"
    timeout 10 docker rm -f "$c" 2>&1 | tail -1 || true
done

write_status "restart" 88 "Container werden neu erstellt (max. 60s)..."
RESTART_START=$(date +%s)
# v6.80: HARTER TIMEOUT auch für up -d — kann auch hängen wenn ein Container nicht hochkommt.
# 60s sind genug für normale Starts; bei Hänger greift der Watchdog.
timeout 60 docker compose -f "$COMPOSE_FILE" up -d --remove-orphans 2>&1 | tail -10 \
    || fail "Container-Start fehlgeschlagen oder Timeout (60s)"
RESTART_TIME=$(($(date +%s) - RESTART_START))
echo "✓ Container gestartet in ${RESTART_TIME}s"

# v6.80: Periodische Status-Updates während wir auf Backend warten,
# damit der Watchdog nicht denkt wir hängen
write_status "verify" 90 "Container gestartet, warte auf Backend..."

# ─── 8. Warten auf Backend ────────────────────────────────
# v6.80: Periodische Status-Updates damit der Watchdog das nicht für tot hält
write_status "verify" 92 "Warte auf Backend-Start..."
sleep 8
BACKEND_READY=0
for i in $(seq 1 60); do
    sleep 2
    # Alle 10s ein Status-Update schreiben (= Watchdog hands-off)
    if [ $((i % 5)) -eq 0 ]; then
        write_status "verify" 92 "Warte auf Backend-Start (${i}/60)..."
    fi
    if docker ps --format '{{.Names}}' | grep -qx 'admin-server-backend'; then
        if curl -fsk http://localhost/api/health >/dev/null 2>&1 \
        || curl -fsk http://localhost:8001/health >/dev/null 2>&1 \
        || docker exec admin-server-backend curl -fs http://localhost:8001/health >/dev/null 2>&1; then
            echo "✓ Backend online nach $((8 + i*2))s"
            BACKEND_READY=1
            break
        fi
    fi
    if ! docker ps -a --format '{{.Names}}' | grep -qE '(^|_)admin-server-backend$'; then
        fail "Backend-Container existiert nicht mehr"
    fi
done

# ─── 9. Verifizieren ───────────────────────────────────────
write_status "verify" 97 "Verifiziere Version..."
BACKEND_CONTAINER=$(docker ps --format '{{.Names}}' | grep -E '(^|_)admin-server-backend$' | head -1)
[ -z "$BACKEND_CONTAINER" ] && BACKEND_CONTAINER=$(docker ps --format '{{.Names}} {{.Image}}' | grep 'shiftdeck-backend' | awk '{print $1}' | head -1)
[ -z "$BACKEND_CONTAINER" ] && fail "Backend-Container nicht gefunden"

INSTALLED=$(docker exec "$BACKEND_CONTAINER" cat /app/VERSION 2>/dev/null | tr -d '[:space:]')
echo "→ Installierte Version: $INSTALLED (erwartet: $TARGET_VERSION)"

if [ "$INSTALLED" = "$TARGET_VERSION" ]; then
    # Alte Backups aufräumen (behalte 5 neueste)
    if [ -d "$BACKUP_DIR" ]; then
        cd "$BACKUP_DIR"
        ls -1t 2>/dev/null | tail -n +6 | while read old; do
            [ -d "$old" ] && rm -rf "$old" && echo "  ✓ Gelöscht: $old"
        done
    fi

    # v6.80: Watchdog-Timer auto-installieren / aktualisieren
    # Damit ab jetzt jedes hängende Update automatisch repariert wird.
    # Wir kopieren IMMER neu (auch wenn Timer schon läuft) damit
    # Intervall-Änderungen (z.B. von 2min auf 30s) ankommen.
    if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then
        echo "→ Aktualisiere Watchdog-Timer..."
        cp "$INSTALL_DIR/bin/shiftdeck-watchdog.service" /etc/systemd/system/ 2>/dev/null || true
        cp "$INSTALL_DIR/bin/shiftdeck-watchdog.timer" /etc/systemd/system/ 2>/dev/null || true
        systemctl daemon-reload 2>/dev/null || true
        # Bei laufendem Timer: restart damit neues Intervall greift
        if systemctl is-active --quiet shiftdeck-watchdog.timer 2>/dev/null; then
            systemctl restart shiftdeck-watchdog.timer 2>/dev/null && \
                echo "✓ Watchdog-Timer neu geladen (alle 30s)" || true
        else
            systemctl enable --now shiftdeck-watchdog.timer 2>/dev/null \
                && echo "✓ Watchdog aktiviert — repariert hängende Updates automatisch alle 30s" \
                || echo "⚠ Watchdog konnte nicht aktiviert werden (kein Problem, Update ist trotzdem fertig)"
        fi
    fi

    write_status "success" 100 "Update erfolgreich! Version v$TARGET_VERSION läuft."
    echo "========== ERFOLG: v$TARGET_VERSION läuft =========="
    exit 0
else
    fail "Version-Mismatch: erwartet v$TARGET_VERSION, läuft v$INSTALLED"
fi
