#!/usr/bin/env bash
# Nimmt ein weiteres Frontend in den Verbund auf.
#
#   frontend-aufnehmen.sh pruefen
#   frontend-aufnehmen.sh anlegen [--quelle WEB-SRV-2] [--name WEB-SRV-3] [--trocken]
#
# GRUNDSATZ: Ein neuer Knoten wird von einem laufenden Frontend GEKLONT, nicht
# aus dem Repository gebaut. Auf den Frontends liegt mehr als der Quelltext –
# die installierte Shopware-Fassung mit ihren Abhängigkeiten, die .env.local
# mit den Zugängen zu Datenbank, Redis und Object Storage, die Einstellungen
# von nginx, PHP-FPM und Varnish. Ein aus dem Repository gebauter Knoten wäre
# ein anderer Server, der nur so aussieht wie die anderen.
#
# Ablauf:
#   1. Vorbedingungen und Gleichheit der vorhandenen Knoten prüfen
#   2. Schnappschuss der Vorlage
#   3. Server daraus erzeugen, ins interne Netz mit fester Adresse
#   4. Klon-Spuren beseitigen (Rechnername, Maschinenkennung, SSH-Wirtsschlüssel)
#   5. Knotenabhängiges setzen: eigener Galera-Knoten zum Schreiben
#   6. Varnish aufwärmen
#   7. ERST DANN das Label "Webserver" – damit hängen sich Firewall,
#      Load Balancer und Varnish-Löschliste von selbst ein
#
# Warum das Label zuletzt kommt: Ein frischer Knoten hat einen leeren
# Varnish. Bei "least_connections" bekommt er sofort Verkehr und liefert
# lauter Cache-Fehlschläge – also die schlechtesten Antwortzeiten genau
# dann, wenn man Last abfedern wollte.
set -uo pipefail

cd "$(dirname "$0")"
source ./lib-knoten.sh

NETZ_ID=11975902          # Internal-1
NETZ_PRAEFIX=10.0.1       # Frontend-Teilnetz
DB_PRAEFIX=10.0.3         # Galera-Knoten
DB_ANZAHL=3
SHOP=/var/www/riccardo
PHP=/usr/bin/php8.5
HOST_KOPF=shop.riccardo-zigarette.de

BEFEHL="${1:-pruefen}"; shift || true
QUELLE_NAME=""; NEU_NAME=""; NEU_IP=""; TROCKEN=0; OHNE_RUECKFRAGE=${OHNE_RUECKFRAGE:-0}
while [ $# -gt 0 ]; do
    case "$1" in
        --quelle) QUELLE_NAME="$2"; shift 2 ;;
        --name)   NEU_NAME="$2"; shift 2 ;;
        --ip)     NEU_IP="$2"; shift 2 ;;
        --trocken) TROCKEN=1; shift ;;
        --ja)      OHNE_RUECKFRAGE=1; shift ;;
        *) echo "Unbekannt: $1"; exit 1 ;;
    esac
done

api() {   # api METHODE PFAD [JSON]
    local m="$1" p="$2" body="${3:-}" token
    token="$(rz_token)" || return 1
    if [ -n "$body" ]; then
        curl -sS -X "$m" -H "Authorization: Bearer $token" -H "Content-Type: application/json" \
             -d "$body" "https://api.hetzner.cloud/v1$p"
    else
        curl -sS -X "$m" -H "Authorization: Bearer $token" "https://api.hetzner.cloud/v1$p"
    fi
}

jq_py() { python3 -c "import json,sys; d=json.load(sys.stdin); $1" ; }

warte_auf_aktion() {   # warte_auf_aktion <aktions-id> <beschreibung>
    local id="$1" was="$2" status n=0
    while :; do
        status=$(api GET "/actions/$id" | jq_py 'print(d["action"]["status"], d["action"].get("progress",0))' 2>/dev/null)
        # Leere Antwort heißt: die API hat gerade nicht geliefert. Nicht
        # abbrechen – sonst steht ein halb erzeugter Server herum –,
        # sondern noch einmal fragen.
        [ -n "$status" ] || { sleep 5; n=$((n+1)); [ "$n" -gt 120 ] && { echo; echo "   $was: keine Antwort der API"; return 1; }; continue; }
        set -- $status
        [ "$1" = "running" ] || break
        n=$((n+1))
        printf '\r   %s … %s%%  (%ds)' "$was" "$2" "$((n*5))"
        sleep 5
    done
    echo
    [ "$1" = "success" ] || { echo "   $was fehlgeschlagen: $1"; return 1; }
}

# ---------------------------------------------------------------- prüfen ----
pruefen() {
    local fehler=0
    echo "== Vorbedingungen"
    rz_pruefe_schluessel && echo "   Schlüssel: $RZ_SCHLUESSEL" || fehler=1
    rz_token >/dev/null && echo "   Hetzner-Token: vorhanden" || fehler=1
    [ "$fehler" = 0 ] || return 1

    mapfile -t KNOTEN < <(rz_knoten) || return 1
    echo "   Knoten mit Label \"$RZ_LABEL\": ${#KNOTEN[@]}"

    echo "== Erreichbarkeit und Rolle"
    local mit_planer=0 ohne_planer=()
    for eintrag in "${KNOTEN[@]}"; do
        set -- $eintrag; local name="$1" ip="$2"
        local planer
        planer=$(rz_ssh "$ip" 'systemctl is-enabled rz-scheduler.service 2>/dev/null || echo nein') || {
            echo "   $name ($ip): nicht erreichbar"; fehler=1; continue; }
        if [ "$planer" = enabled ]; then
            printf '   %-12s %-10s Zeitplaner: ja\n' "$name" "$ip"; mit_planer=$((mit_planer+1))
        else
            printf '   %-12s %-10s Zeitplaner: nein\n' "$name" "$ip"; ohne_planer+=("$name $ip")
        fi
    done
    if [ "$mit_planer" != 1 ]; then
        echo "   FEHLER: Der Zeitplaner muss auf GENAU EINEM Knoten laufen, gezählt: $mit_planer"
        fehler=1
    fi
    [ "${#ohne_planer[@]}" -gt 0 ] || { echo "   FEHLER: Kein Knoten ohne Zeitplaner – keine Vorlage zum Klonen."; fehler=1; }

    echo "== Sind die vorhandenen Knoten gleich?"
    # Nur DATEIEN vergleichen, keine Verzeichnisse: deren gemeldete Größe ist
    # die Blockbelegung und weicht ab, sobald in einem Ordner einmal mehr
    # Einträge lagen. Das sah beim ersten Versuch nach echtem Unterschied aus.
    local summe erste="" abweichung=0
    for eintrag in "${KNOTEN[@]}"; do
        set -- $eintrag; local name="$1" ip="$2"
        summe=$(rz_ssh "$ip" "cd $SHOP && find . -type f -not -path './var/*' -not -path './public/theme/*' -not -name varnish.yaml -printf '%P %s\n' | sort | md5sum | cut -c1-12")
        [ -n "$erste" ] || erste="$summe"
        [ "$summe" = "$erste" ] && printf '   %-12s %s\n' "$name" "$summe" \
            || { printf '   %-12s %s  ABWEICHUNG\n' "$name" "$summe"; abweichung=1; }
    done
    if [ "$abweichung" = 1 ]; then
        echo "   FEHLER: Die Knoten stehen nicht auf demselben Stand. Erst angleichen, dann klonen –"
        echo "           sonst vererbt der Klon den Rückstand eines einzelnen Servers."
        fehler=1
    fi
    # varnish.yaml ist bewusst ausgenommen: sie wird erzeugt und trägt einen
    # Zeitstempel im Kopf.

    echo "== Platz im Netz"
    local belegt frei=""
    belegt=$(printf '%s\n' "${KNOTEN[@]}" | awk '{print $2}')
    for n in $(seq 1 20); do
        printf '%s\n' "$belegt" | grep -qx "$NETZ_PRAEFIX.$n" || { frei="$NETZ_PRAEFIX.$n"; break; }
    done
    [ -n "$frei" ] && echo "   nächste freie Adresse: $frei (Galera-Knoten $DB_PRAEFIX.$(( (${frei##*.} - 1) % DB_ANZAHL + 1 )))" \
                   || { echo "   FEHLER: keine freie Adresse"; fehler=1; }

    echo "== Automatik, die am Label hängt"
    api GET "/load_balancers" | jq_py '
for lb in d["load_balancers"]:
    for t in lb["targets"]:
        if t["type"] == "label_selector":
            print("   Load Balancer %s: Ziele über Label \"%s\", derzeit %d" % (lb["name"], t["label_selector"]["selector"], len(t.get("targets",[]))))'
    api GET "/firewalls" | jq_py '
for f in d["firewalls"]:
    for a in f.get("applied_to",[]):
        if a["type"] == "label_selector" and a["label_selector"]["selector"] == "Webserver":
            print("   Firewall %s: hängt am Label \"Webserver\"" % f["name"])'

    [ "$fehler" = 0 ] && echo "== Bereit." || echo "== NICHT bereit."
    return $fehler
}

# --------------------------------------------------------------- anlegen ----
anlegen() {
    pruefen || { echo "Abgebrochen."; return 1; }
    echo

    mapfile -t KNOTEN < <(rz_knoten)
    # Vorlage: ein Knoten OHNE Zeitplaner. Der Zeitplaner darf sich nicht
    # vervielfachen – ein Klon des Knotens, der ihn trägt, brächte einen
    # zweiten mit, und dann laufen alle geplanten Aufgaben doppelt.
    local quelle_name="" quelle_ip=""
    for eintrag in "${KNOTEN[@]}"; do
        set -- $eintrag
        if [ -n "$QUELLE_NAME" ]; then
            [ "$1" = "$QUELLE_NAME" ] && { quelle_name="$1"; quelle_ip="$2"; }
        else
            [ "$(rz_ssh "$2" 'systemctl is-enabled rz-scheduler.service 2>/dev/null || echo nein')" = enabled ] \
                || { quelle_name="$1"; quelle_ip="$2"; break; }
        fi
    done
    [ -n "$quelle_name" ] || { echo "Keine Vorlage gefunden."; return 1; }

    if [ "$(rz_ssh "$quelle_ip" 'systemctl is-enabled rz-scheduler.service 2>/dev/null || echo nein')" = enabled ]; then
        echo "FEHLER: $quelle_name trägt den Zeitplaner und taugt nicht als Vorlage."
        return 1
    fi

    local belegt frei=""
    belegt=$(printf '%s\n' "${KNOTEN[@]}" | awk '{print $2}')
    for n in $(seq 1 20); do
        printf '%s\n' "$belegt" | grep -qx "$NETZ_PRAEFIX.$n" || { frei="$NETZ_PRAEFIX.$n"; break; }
    done
    [ -n "$NEU_IP" ] && frei="$NEU_IP"
    local nr="${frei##*.}"
    local db_nr=$(( (nr - 1) % DB_ANZAHL + 1 ))
    [ -n "$NEU_NAME" ] || NEU_NAME="WEB-SRV-$nr"

    # Typ, Ort und Platzgruppe der Vorlage übernehmen. Die API liefert den
    # Ort mal unter "location", mal unter "datacenter" – beides abfangen.
    local eck
    eck=$(api GET "/servers?name=$quelle_name" | jq_py '
s = d["servers"][0]
ort = (s.get("location") or (s.get("datacenter") or {}).get("location") or {}).get("name", "")
print(s["id"], s["server_type"]["name"], ort, (s.get("placement_group") or {}).get("id", "-"))')
    [ -n "$eck" ] || { echo "Konnte die Eckdaten von $quelle_name nicht lesen."; return 1; }
    set -- $eck; local q_id="$1" typ="$2" ort="$3" gruppe="$4"
    [ -n "$ort" ] || { echo "Kein Ort für $quelle_name ermittelbar."; return 1; }

    cat <<INFO

== Vorhaben
   Vorlage      : $quelle_name ($quelle_ip), Typ $typ, Ort $ort
   Neuer Knoten : $NEU_NAME, interne Adresse $frei
   Schreibt auf : $DB_PRAEFIX.$db_nr
   Platzgruppe  : $gruppe
   Label        : wird ERST am Ende gesetzt

   Das erzeugt einen kostenpflichtigen Server und einen Schnappschuss.
INFO
    if [ "$TROCKEN" = 1 ]; then echo "   Trockenlauf – hier ist Schluss."; return 0; fi

    # --ja überspringt die Rückfrage. Gedacht für den Agenten, der einen im
    # Admin angeforderten Auftrag abarbeitet – dort hat der Mensch bereits
    # im Dialog zugestimmt, und eine Rückfrage an ein Skript ohne Tastatur
    # wäre nur eine Sitzung, die stehen bleibt.
    if [ "$OHNE_RUECKFRAGE" = 1 ]; then
        echo "   (--ja: ohne Rückfrage)"
    else
        read -r -p "   Weiter? [tippe JA] " antwort
        [ "$antwort" = "JA" ] || { echo "Abgebrochen."; return 1; }
    fi

    echo
    echo "== 1. Schnappschuss von $quelle_name"
    # Im laufenden Betrieb: Der Zustand des Shops liegt in Datenbank, Redis
    # und Object Storage, auf der Platte steht im Wesentlichen unveraenderlicher
    # Code. Herunterfahren waere sauberer, nimmt den Knoten aber fuer Minuten
    # aus dem Load Balancer.
    local antwort_json bild_id aktion_id
    antwort_json=$(api POST "/servers/$q_id/actions/create_image" \
        "{\"type\":\"snapshot\",\"description\":\"Vorlage fuer $NEU_NAME ($(date +%F))\"}")
    bild_id=$(printf '%s' "$antwort_json" | jq_py 'print(d["image"]["id"])') || { echo "$antwort_json"; return 1; }
    aktion_id=$(printf '%s' "$antwort_json" | jq_py 'print(d["action"]["id"])')
    warte_auf_aktion "$aktion_id" "Schnappschuss" || return 1
    echo "   Bild-Id: $bild_id"

    echo "== 2. Server erzeugen"
    [ "$gruppe" = "-" ] && gruppe=null
    antwort_json=$(api POST "/servers" "{
        \"name\": \"$NEU_NAME\",
        \"server_type\": \"$typ\",
        \"image\": $bild_id,
        \"location\": \"$ort\",
        \"placement_group\": $gruppe,
        \"start_after_create\": true,
        \"labels\": {}
    }")
    local neu_id
    neu_id=$(printf '%s' "$antwort_json" | jq_py 'print(d["server"]["id"])') || { echo "$antwort_json"; return 1; }
    aktion_id=$(printf '%s' "$antwort_json" | jq_py 'print(d["action"]["id"])')
    warte_auf_aktion "$aktion_id" "Server anlegen" || return 1
    echo "   Server-Id: $neu_id"

    echo "== 3. Ins interne Netz mit fester Adresse $frei"
    antwort_json=$(api POST "/servers/$neu_id/actions/attach_to_network" \
        "{\"network\": $NETZ_ID, \"ip\": \"$frei\"}")
    aktion_id=$(printf '%s' "$antwort_json" | jq_py 'print(d["action"]["id"])') || { echo "$antwort_json"; return 1; }
    warte_auf_aktion "$aktion_id" "Netz anhängen" || return 1

    # Neustart, damit der Gast die neue Schnittstelle sauber bekommt.
    aktion_id=$(api POST "/servers/$neu_id/actions/reboot" | jq_py 'print(d["action"]["id"])')
    warte_auf_aktion "$aktion_id" "Neustart" || return 1

    echo "== 4. Warten, bis der Knoten antwortet"
    local n=0
    until rz_ssh "$frei" 'true' 2>/dev/null; do
        n=$((n+1)); [ "$n" -gt 60 ] && { echo "   Keine Anmeldung nach 5 Minuten – von Hand nachsehen."; return 1; }
        printf '\r   warte … %ds' "$((n*5))"; sleep 5
    done
    echo -e "\r   erreichbar        "

    echo "== 5. Klon-Spuren beseitigen und knotenabhängiges setzen"
    rz_ssh_stdin "$frei" 'bash -s' <<REMOTE
set -e
hostnamectl set-hostname "$NEU_NAME"
sed -i "s/\b$quelle_name\b/$NEU_NAME/g" /etc/hosts

# Maschinenkennung und SSH-Wirtsschlüssel stammen aus dem Klon und wären
# sonst auf zwei Servern gleich.
rm -f /etc/machine-id /var/lib/dbus/machine-id
systemd-machine-id-setup >/dev/null
rm -f /etc/ssh/ssh_host_*
ssh-keygen -A >/dev/null
systemctl restart ssh 2>/dev/null || systemctl restart sshd

# Galera: dieser Knoten schreibt auf seinen eigenen Datenbankknoten und
# liest von den beiden anderen.
cd $SHOP
sed -i -E "s#(DATABASE_URL=mysql://[^@]+@)[0-9.]+#\1$DB_PRAEFIX.$db_nr#" .env.local
i=0
for k in \$(seq 1 $DB_ANZAHL); do
    # Bewusst if statt "[ … ] && continue": mit set -e beendet ein nicht
    # zutreffender Test die ganze Sitzung, und der Knoten bliebe halbfertig.
    if [ "\$k" != "$db_nr" ]; then
        sed -i -E "s#(DATABASE_REPLICA_\${i}_URL=mysql://[^@]+@)[0-9.]+#\1$DB_PRAEFIX.\$k#" .env.local
        i=\$((i+1))
    fi
done
# Gegenprobe: steht der eigene Knoten wirklich in DATABASE_URL?
grep -q "DATABASE_URL=mysql://[^@]*@$DB_PRAEFIX.$db_nr:" .env.local \
    || { echo "   FEHLER: DATABASE_URL zeigt nicht auf $DB_PRAEFIX.$db_nr"; exit 1; }
sed -i "s/^# Galera:.*/# Galera: dieser Knoten schreibt auf $DB_PRAEFIX.$db_nr/" .env.local

# Der Zeitplaner darf nur auf einem Knoten laufen.
systemctl disable --now rz-scheduler.service 2>/dev/null || true

sudo -u www-data $PHP bin/console cache:clear -q
systemctl restart php8.5-fpm nginx varnish
echo "   Rechnername: \$(hostname), schreibt auf $DB_PRAEFIX.$db_nr"
REMOTE

    echo "== 6. Prüfen, ob der Shop auf dem Knoten antwortet"
    local code
    code=$(rz_ssh "$frei" "curl -s -o /dev/null -w '%{http_code}' -H 'Host: $HOST_KOPF' http://127.0.0.1/")
    echo "   Antwort über Varnish: HTTP $code"
    [ "$code" = 200 ] || { echo "   Kein 200 – Label NICHT gesetzt, der Knoten bleibt außen vor."; return 1; }

    echo "== 7. Varnish aufwärmen"
    rz_ssh "$frei" "for p in / /Liquids/ /Aromen/ /E-Zigaretten/ /Bundles/; do
        curl -s -o /dev/null -H 'Host: $HOST_KOPF' http://127.0.0.1\$p; done; echo '   Startseite und vier Kategorien geholt'"

    echo "== 8. Label setzen – jetzt hängen sich Firewall, Load Balancer und Löschliste ein"
    api PUT "/servers/$neu_id" '{"labels":{"Webserver":"","Riccardo-Onlineshop":""}}' >/dev/null
    sleep 5
    ./varnish-knoten.sh setzen

    echo "== 9. Abschlussprüfung"
    ./auf-allen-frontends.sh "echo \"\$(hostname): Shop \$(curl -s -o /dev/null -w '%{http_code}' -H 'Host: $HOST_KOPF' http://127.0.0.1/), Varnish \$(systemctl is-active varnish)\""
    echo
    echo "Fertig. Nicht vergessen: der Zeitplaner läuft weiterhin nur auf einem Knoten."
}

case "$BEFEHL" in
    pruefen) pruefen ;;
    anlegen) anlegen ;;
    *) echo "Aufruf: $0 pruefen | anlegen [--quelle NAME] [--name NAME] [--ip ADRESSE] [--trocken] [--ja]"; exit 1 ;;
esac
