# shellcheck shell=sh
#
# Die Übergabe: Proxmox-Rolle und Token erzeugen, bei CluPilot registrieren, den
# WireGuard-Schlüssel tauschen.
#
# ---------------------------------------------------------------------------
# Die Rolle, und warum `|| true` hier NICHT steht
# ---------------------------------------------------------------------------
#
# `pveum role add … || true` wendet die Rechteliste nur bei dem Lauf an, der die
# Rolle zum ersten Mal anlegt. Ein Recht, das später dazukam, erreichte damit
# neue Hosts und keinen einzigen bestehenden. Genau so fehlte `Sys.Modify`
# überall — mit einem Symptom, das viel später und ganz woanders auftrat: ein
# 403 auf `POST /cluster/backup`, und die Bereitstellung eines zahlenden Kunden
# scheiterte bei `register_backup`.
#
# Deshalb: `role add`, und wenn das scheitert (die Rolle gibt es schon, der
# gewöhnliche Fall), `role modify` — zwei getrennte Befehle, damit ein
# Fehlschlag zuzuordnen ist. `modify` ohne `-append` ERSETZT die Liste, der Host
# passt sich also der Konfiguration an, statt anzusammeln, was irgendeine
# frühere Fassung einmal gewährt hat.
#
# Für Benutzer und ACL galt `|| true` bisher als geduldet, weil „gibt es schon"
# dort der Normalfall ist. Auch das entfällt: statt den Fehler zu verwerfen,
# wird nachgesehen, ob der gewünschte Zustand steht. Ein `|| true` verdeckt
# nicht nur „gibt es schon", sondern auch „ging schief".

CLUPILOT_PVE_ROLE='CluPilotAutomation'
CLUPILOT_PVE_USER='automation@pve'
CLUPILOT_PVE_TOKEN_NAME='clupilot'

# Wörtlich aus `config/provisioning.php`. Sys.Modify ist nicht optional: es steht
# in `PVE/API2/Backup.pm` als `check => ['perm', '/', ['Sys.Modify']]` — an der
# Quelle geprüft, nicht aus einem Forenbeitrag geschlossen — und deckt zugleich
# `PUT /cluster/firewall/options` ab. Gewährt auf `/`, weil beide Endpunkte
# gegen `/` prüfen und nichts Engeres sie zufriedenstellt.
CLUPILOT_PVE_PRIVS='VM.Allocate,VM.Clone,VM.Config.Disk,VM.Config.CPU,VM.Config.Memory,VM.Config.Network,VM.Config.Options,VM.Config.Cloudinit,VM.PowerMgmt,VM.Monitor,VM.Audit,VM.Backup,VM.GuestAgent.Audit,VM.GuestAgent.Unrestricted,Datastore.AllocateSpace,Datastore.Audit,Sys.Audit,Sys.Modify'

# Ein Feld aus flachem JSON. Nimmt `jq`, wenn es da ist, und sonst einen
# eng gefassten Ausdruck.
json_field() {
    _key="$1"
    _json="$2"

    if command -v jq >/dev/null 2>&1; then
        printf '%s' "$_json" | jq -r --arg k "$_key" '.[$k] // empty' 2>/dev/null
        return 0
    fi

    printf '%s' "$_json" \
        | sed -n "s/.*\"${_key}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" \
        | head -1
}

converge_proxmox_role() {
    if pveum role add "$CLUPILOT_PVE_ROLE" -privs "$CLUPILOT_PVE_PRIVS" >/dev/null 2>&1; then
        log "Proxmox-Rolle ${CLUPILOT_PVE_ROLE} angelegt"
        return 0
    fi

    if pveum role modify "$CLUPILOT_PVE_ROLE" -privs "$CLUPILOT_PVE_PRIVS" >/dev/null 2>&1; then
        log "Proxmox-Rolle ${CLUPILOT_PVE_ROLE} auf die aktuelle Rechteliste gebracht"
        return 0
    fi

    log 'Die Rechteliste der Proxmox-Rolle ließ sich nicht angleichen'
    return 1
}

# Benutzer und ACL — ohne `|| true`, aber mit Nachsehen.
ensure_proxmox_user() {
    if ! pveum user list --output-format json 2>/dev/null | grep -q "\"${CLUPILOT_PVE_USER}\""; then
        if ! pveum user add "$CLUPILOT_PVE_USER" >/dev/null 2>&1; then
            # Nicht blind schlucken: nachsehen, ob er jetzt da ist. Wenn ja, war
            # es ein Wettlauf und kein Fehler.
            if ! pveum user list --output-format json 2>/dev/null | grep -q "\"${CLUPILOT_PVE_USER}\""; then
                log "Proxmox-Benutzer ${CLUPILOT_PVE_USER} ließ sich nicht anlegen"
                return 1
            fi
        fi
    fi

    if ! pveum acl modify / -user "$CLUPILOT_PVE_USER" -role "$CLUPILOT_PVE_ROLE" >/dev/null 2>&1; then
        # Auch hier: gilt die Zuweisung inzwischen, war es keiner.
        if ! pveum acl list --output-format json 2>/dev/null | grep -q "$CLUPILOT_PVE_ROLE"; then
            log 'Die Rollenzuweisung auf / ließ sich nicht setzen'
            return 1
        fi
    fi

    return 0
}

# Erzeugt den API-Token und gibt "<tokenid>=<secret>" aus.
#
# Das Geheimnis wird genau einmal gezeigt. Ein halb angelegter Token aus einem
# abgebrochenen früheren Lauf wird vorher entfernt, damit ein zweiter Lauf
# gleich ausgeht.
mint_proxmox_token() {
    pveum user token remove "$CLUPILOT_PVE_USER" "$CLUPILOT_PVE_TOKEN_NAME" >/dev/null 2>&1 || true

    _out="$(pveum user token add "$CLUPILOT_PVE_USER" "$CLUPILOT_PVE_TOKEN_NAME" -privsep 0 --output-format json 2>/dev/null)"
    [ -n "$_out" ] || return 1

    _secret="$(json_field value "$_out")"
    [ -n "$_secret" ] || return 1

    _tokenid="$(json_field 'full-tokenid' "$_out")"
    [ -n "$_tokenid" ] || _tokenid="${CLUPILOT_PVE_USER}!${CLUPILOT_PVE_TOKEN_NAME}"

    printf '%s=%s' "$_tokenid" "$_secret"
}

# Der Fingerabdruck des SSH-Wirtsschlüssels. Nur zur Anzeige — die Plattform
# meldet sich nicht mehr per SSH an (Spec §6).
ssh_host_fingerprint() {
    for _key in /etc/ssh/ssh_host_ed25519_key.pub /etc/ssh/ssh_host_rsa_key.pub; do
        if [ -f "$_key" ]; then
            ssh-keygen -lf "$_key" 2>/dev/null | awk '{ print $2; exit }'
            return 0
        fi
    done
    printf ''
}

# Die Eckdaten dieses Knotens, so wie `RegisterCapacity` sie liest.
#
# Der größte Speicher, der Platten aufnimmt — NICHT die Summe. Proxmox führt
# überlappende Pools (local und local-lvm auf derselben Platte), und zu
# summieren hieße, denselben Platz zweimal zu verkaufen.
node_total_gb() {
    pvesm status --content images 2>/dev/null \
        | awk 'NR > 1 { if ($4 + 0 > max) max = $4 + 0 } END { printf "%d", max / 1048576 }'
}

# Baut den Rumpf für POST /host/register.
render_register_payload() {
    _api_token="$1"
    _wg_pubkey="$2"

    printf '{"code":"%s","api_token":"%s","wg_pubkey":"%s","node":"%s","pve_version":"%s","total_gb":%s,"total_ram_mb":%s,"cpu_cores":%s,"ssh_host_key":"%s"}' \
        "$(json_escape "$CLUPILOT_CODE")" \
        "$(json_escape "$_api_token")" \
        "$(json_escape "$_wg_pubkey")" \
        "$(json_escape "$(hostname)")" \
        "$(json_escape "$(pveversion 2>/dev/null | head -1)")" \
        "$(node_total_gb)" \
        "$(awk '/^MemTotal:/ { printf "%d", $2 / 1024; exit }' /proc/meminfo)" \
        "$(nproc 2>/dev/null || echo 1)" \
        "$(json_escape "$(ssh_host_fingerprint)")"
}

# POST /host/register — gibt die Antwort aus, oder 1.
post_registration() {
    _payload="$1"

    if command -v curl >/dev/null 2>&1; then
        curl -sS -f --connect-timeout 10 --max-time 60 \
            -H 'Content-Type: application/json' -H 'Accept: application/json' \
            -X POST --data "$_payload" \
            "${CLUPILOT_API}/host/register" 2>/dev/null
        return $?
    fi

    wget -q -O - --timeout=60 \
        --header='Content-Type: application/json' --header='Accept: application/json' \
        --post-data="$_payload" \
        "${CLUPILOT_API}/host/register" 2>/dev/null
}

# ---------------------------------------------------------------------------
# Der Schlüsseltausch
# ---------------------------------------------------------------------------
#
# Task 6 lässt den Tunnel erst mit bewiesenem Handshake gelten. Hier wird genau
# dieser Tunnel im laufenden Betrieb auf einen neuen Schlüssel umgestellt — also
# gilt dieselbe Regel, und die Reihenfolge allein genügt nicht: sie sagt, WANN
# verworfen wird, nicht OB das Neue trägt.
#
#   1. neues Paar erzeugen, registrieren, Antwort haben
#   2. wg0 auf den neuen privaten Schlüssel umstellen
#   3. erneuten Handshake BEWEISEN
#   4. erst dann den alten verwerfen
#
# Kommt der Handshake nicht, wird zurückgestellt und laut gescheitert. Ein Host,
# der sich im letzten Schritt selbst aussperrt, ist der eine Fall, den niemand
# aus der Ferne repariert — und er passiert am Ende eines Laufs, der bis dahin
# alles richtig gemacht hat.
swap_wireguard_key() {
    _new_private="$1"
    _old_private="$CLUPILOT_WG_PRIVATE"

    CLUPILOT_WG_PRIVATE="$_new_private"
    printf '%s\n' "$(render_wireguard_config)" > /etc/wireguard/wg0.conf
    chmod 600 /etc/wireguard/wg0.conf

    systemctl restart wg-quick@wg0 >/dev/null 2>&1 || true

    if wireguard_handshake_proven 10; then
        log 'Handshake mit dem neuen Schlüssel bewiesen'
        return 0
    fi

    log 'Kein Handshake mit dem neuen Schlüssel — es wird auf den alten zurückgestellt'
    CLUPILOT_WG_PRIVATE="$_old_private"
    printf '%s\n' "$(render_wireguard_config)" > /etc/wireguard/wg0.conf
    chmod 600 /etc/wireguard/wg0.conf
    systemctl restart wg-quick@wg0 >/dev/null 2>&1 || true

    return 1
}
