#!/usr/bin/env bash
# =============================================================================
# Helix V5 — golden/restore.sh
#
# Spielt ein Golden Image aus dem Image-Store des Gateways auf Box 1 zurueck.
#
# LAEUFT ALS ROOT IN DER CASPER-LIVE-UMGEBUNG des Ubuntu-24.04-Live-Servers,
# aufgerufen aus den autoinstall-`early-commands` des Restore-Seeds
# (/golden/user-data). Die early-commands laufen, BEVOR Subiquity irgendetwas
# am Datentraeger anfasst — und dieses Skript rebootet am Ende, deshalb kommt
# Subiquity nie zum Zug.
#
# Aufruf (so steht es im Seed):
#   curl -fsS https://images.v5.helix-framework.de/golden/restore.sh | bash -s -- --version v1
#
# Ablauf und Statusmeldungen (POST /register, Feld stage):
#   restore-started      Start, Abhaengigkeiten da, Metadaten geladen
#   restore-partitioned  GPT, Dateisysteme (mit ERZWUNGENEN UUIDs), LVM stehen
#   restore-downloaded   Archive geladen und per SHA256SUMS geprueft
#   restore-extracted    root/boot/efi entpackt
#   restore-grub-done    Bootloader installiert, Reprovision-Eintrag gesetzt
#   restore-failed-<grund>  Abbruch (kein Reboot, Live-Umgebung bleibt oben)
#
# ACHTUNG: Dieses Skript LOESCHT die Zielplatte vollstaendig. Es laeuft nur in
# einer Live-Umgebung (Guard unten) und niemals auf einem laufenden System.
# =============================================================================
set -euo pipefail

GATEWAY_PROV="${GATEWAY_PROV:-images.v5.helix-framework.de}"
STORE_BASE="${STORE_BASE:-https://${GATEWAY_PROV}/golden}"
REGISTER_URL="${REGISTER_URL:-https://${GATEWAY_PROV}/register}"
SEED_BASE="${SEED_BASE:-${STORE_BASE}/}"       # Restore-Seed (fuer den Reprovision-Eintrag)
NODE_NAME="${NODE_NAME:-helix-ai-1}"
TARGET=/mnt
META_DIR=/tmp/golden-meta
DL_DIR="${TARGET}/var/tmp/restore"

VERSION=""
while [ $# -gt 0 ]; do
  case "$1" in
    --version) VERSION="${2:-}"; shift 2 ;;
    *) echo "unbekannter Parameter: $1" >&2; exit 2 ;;
  esac
done
[ -n "$VERSION" ] || { echo "--version ist Pflicht (z. B. --version v1)" >&2; exit 2; }
case "$VERSION" in
  v[0-9]|v[0-9][0-9]) : ;;
  *) echo "--version muss dem Muster vN entsprechen — bekommen: ${VERSION}" >&2; exit 2 ;;
esac

PREFIX="node-golden-${VERSION}"
SRC="${STORE_BASE}/${VERSION}"

log()  { printf '[restore %s] %s\n' "$(date -u +%H:%M:%S)" "$*"; }
warn() { printf '[restore %s] WARNUNG: %s\n' "$(date -u +%H:%M:%S)" "$*" >&2; }

# ----------------------------------------------------------------------------
# Phone-Home. Fehlschlaege duerfen den Restore nie abbrechen (|| true) — die
# Meldung ist Diagnose, nicht Voraussetzung.
# ----------------------------------------------------------------------------
post_stage() {
  local stage="$1"
  # Der komplette Koerper laeuft in einer Subshell mit ABGESCHALTETEM errexit/
  # pipefail und einem abschliessenden `|| true`. Grund: post_stage wird auch
  # aus dem EXIT-Trap heraus aufgerufen — scheitert hier irgendetwas, wuerde
  # `set -e` den Trap abbrechen und der restore-failed-*-Status ginge NIE raus.
  # Konkreter Fall: `ip route get 1.1.1.1` scheitert bei kaputtem Netz (rc=2),
  # unter pipefail schlug damit die Zuweisung fehl — ausgerechnet im
  # wahrscheinlichsten Fehlerfall (Netz weg) waere die Meldung ausgeblieben.
  # `set +e` in einer Subshell kann die Optionen des Skripts nicht veraendern.
  (
    set +e
    set +o pipefail
    route="$(ip -4 -o route get 1.1.1.1 2>/dev/null)"
    ipaddr="$(sed -n 's/.* src \([0-9.]*\).*/\1/p' <<<"$route")"
    ipaddr="${ipaddr%%$'\n'*}"
    curl -m 15 -fsS -X POST "$REGISTER_URL" \
         --data-urlencode "hostname=${NODE_NAME}" \
         --data-urlencode "ip=${ipaddr:-unknown}" \
         --data-urlencode "stage=${stage}" >/dev/null 2>&1
  ) || true
  log "Status gemeldet: ${stage}"
}

FAILED_REASON=""
cleanup() {
  local rc=$?
  # Ab hier darf NICHTS mehr abbrechen: der Trap muss den failed-Status posten
  # und die Live-Umgebung offen halten, auch wenn einzelne Kommandos scheitern.
  # (Wir sind bereits auf dem Weg nach draussen — errexit hier abzuschalten hat
  # keine Nebenwirkung mehr, verhindert aber einen stillen Trap-Abbruch.)
  set +e
  set +o pipefail
  if [ "$rc" -ne 0 ]; then
    post_stage "restore-failed-${FAILED_REASON:-rc${rc}}"
    warn "ABBRUCH (rc=${rc}, Grund: ${FAILED_REASON:-unbekannt})."
    warn "Es wird NICHT rebootet — die Zielplatte ist moeglicherweise unvollstaendig."
    warn "Die Live-Umgebung bleibt oben. Naechster Schritt: physischer Zugriff auf die Box."
    umount -R "${TARGET}/boot/efi" 2>/dev/null || true
    umount -R "${TARGET}/boot" 2>/dev/null || true
    umount -R "$TARGET" 2>/dev/null || true
    # Live-Umgebung offen halten statt in einen Bootschleifen-Reboot zu laufen.
    sleep infinity
  fi
}
trap cleanup EXIT
# Auch bei Signalen ueber den EXIT-Trap gehen, damit der failed-Status in jedem
# Fall gemeldet wird (ein ungetrapptes SIGTERM/SIGINT beendet die Shell sonst,
# ohne den EXIT-Trap auszufuehren — der Status ginge lautlos verloren).
trap 'exit 1' INT TERM HUP
fail() { FAILED_REASON="$1"; shift; warn "$*"; exit 1; }

# ----------------------------------------------------------------------------
# 0) Guards
# ----------------------------------------------------------------------------
[ "$(id -u)" -eq 0 ] || fail "notroot" "muss als root laufen"

# Nur in einer Live-Umgebung. Auf einem installierten System liegt / auf einem
# echten Blockgeraet (LVM/NVMe); in der casper-Live-Umgebung auf overlay/tmpfs.
ROOT_FS_SRC="$(findmnt -no SOURCE / || echo unknown)"
case "$ROOT_FS_SRC" in
  /dev/nvme*|/dev/sd*|/dev/mapper/*|/dev/vd*)
    fail "notlive" "/ liegt auf ${ROOT_FS_SRC} — das sieht nach einem INSTALLIERTEN System aus. Abbruch (dieses Skript loescht Datentraeger)." ;;
esac
log "Live-Umgebung erkannt (/ auf ${ROOT_FS_SRC})."

# ----------------------------------------------------------------------------
# 1) Abhaengigkeiten. Die casper-Live-Umgebung bringt nicht alles mit
#    (haeufig fehlen zstd und gdisk/sgdisk; efibootmgr und lvm2 sind meist da,
#    weil Subiquity sie braucht). Netz ist zu diesem Zeitpunkt vorhanden
#    (ip=dhcp in der Kernel-Cmdline), deshalb ist apt der einfachste Weg.
# ----------------------------------------------------------------------------
declare -A PKG_FOR=(
  [zstd]=zstd [sgdisk]=gdisk [efibootmgr]=efibootmgr [curl]=curl
  [pvcreate]=lvm2 [mkfs.vfat]=dosfstools [mkfs.ext4]=e2fsprogs
  [python3]=python3-minimal [partprobe]=parted [tar]=tar
)
MISSING=()
for tool in "${!PKG_FOR[@]}"; do
  command -v "$tool" >/dev/null 2>&1 || MISSING+=("${PKG_FOR[$tool]}")
done
if [ "${#MISSING[@]}" -gt 0 ]; then
  # shellcheck disable=SC2207
  MISSING=($(printf '%s\n' "${MISSING[@]}" | sort -u))
  log "Nachinstallieren in der Live-Session: ${MISSING[*]}"
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qq || fail "aptupdate" "apt-get update fehlgeschlagen (kein Netz?)"
  apt-get install -y -qq --no-install-recommends "${MISSING[@]}" \
    || fail "aptinstall" "apt-get install fehlgeschlagen: ${MISSING[*]}"
  for tool in "${!PKG_FOR[@]}"; do
    command -v "$tool" >/dev/null 2>&1 || fail "tool" "Werkzeug weiterhin nicht da: ${tool}"
  done
else
  log "Alle benoetigten Werkzeuge sind vorhanden."
fi

post_stage restore-started

# ----------------------------------------------------------------------------
# 2) Metadaten holen (klein, vor dem Partitionieren noetig)
# ----------------------------------------------------------------------------
log "Lade Metadaten aus ${SRC}/ …"
rm -rf "$META_DIR"; mkdir -p "$META_DIR"; cd "$META_DIR"
for f in SHA256SUMS "${PREFIX}.json" "${PREFIX}-gpt.bin" "${PREFIX}-lvm.vg"; do
  curl -fsS --retry 3 --retry-connrefused -o "$f" "${SRC}/${f}" \
    || fail "metadl" "Download fehlgeschlagen: ${SRC}/${f}"
done
grep -E " (${PREFIX}\.json|${PREFIX}-gpt\.bin|${PREFIX}-lvm\.vg)$" SHA256SUMS > SHA256SUMS.meta \
  || fail "metasums" "SHA256SUMS enthaelt keine Eintraege fuer die Metadatendateien"
sha256sum -c SHA256SUMS.meta >/dev/null || fail "metahash" "Pruefsummen der Metadaten stimmen nicht"
log "Metadaten geladen und geprueft."

md() { python3 -c '
import json,sys
d=json.load(open(sys.argv[1]))
for k in sys.argv[2].split("."):
    d = d[k]
print(d)' "${META_DIR}/${PREFIX}.json" "$1"; }

ESP_VOLID="$(md filesystems.esp.volid)"
ESP_PARTUUID="$(md filesystems.esp.partuuid)"
BOOT_UUID="$(md filesystems.boot.uuid)"
BOOT_PARTUUID="$(md filesystems.boot.partuuid)"
ROOT_UUID="$(md filesystems.root.uuid)"
PV_UUID="$(md lvm.pv_uuid)"
PV_PARTUUID="$(md lvm.pv_partuuid)"
VG_NAME="$(md lvm.vg)"
LV_NAME="$(md lvm.lv)"
LV_SIZE_B="$(md lvm.lv_size_bytes)"
DM_UUID_EXPECTED="$(md lvm.dm_uuid)"
SWAP_PATH="$(md swapfile.path)"
SWAP_SIZE_B="$(md swapfile.size_bytes)"
BOOT_ISO_URL="$(md boot_iso_url)"
CAPTURED_AT="$(md captured_at)"
log "Image ${VERSION} vom ${CAPTURED_AT}: ESP ${ESP_VOLID} · /boot ${BOOT_UUID} · / ${ROOT_UUID}"
log "LVM ${VG_NAME}/${LV_NAME}, $((LV_SIZE_B/1024/1024/1024)) GiB, dm-uuid ${DM_UUID_EXPECTED}"

# ----------------------------------------------------------------------------
# 3) Zielplatte: die groesste NVMe
# ----------------------------------------------------------------------------
# Kein `… | sort -nr | head -1`: `head` beendet sich nach der ersten Zeile und
# schliesst die Pipe — unter `set -o pipefail` kann der Producer (sort) an
# SIGPIPE sterben und die Pipeline meldet trotz korrektem Ergebnis Fehler. Das
# waere hier fatal: Abbruch -> Trap -> `sleep infinity` -> haengende Box, die
# nur noch physisch erreichbar ist. Deshalb Liste komplett einlesen und das
# Maximum in einem awk-Durchlauf bestimmen (awk liest immer bis EOF).
DISK_LIST="$(lsblk -bdn -o NAME,SIZE,TYPE)" || fail "lsblk" "lsblk lieferte keine Geraeteliste"
DISK_NAME="$(awk '$3=="disk" && $1 ~ /^nvme/ && $2+0 > max { max=$2+0; name=$1 } END { print name }' <<<"$DISK_LIST")"
[ -n "$DISK_NAME" ] || fail "nodisk" "keine NVMe gefunden"
DISK="/dev/${DISK_NAME}"
DISK_SIZE_B="$(blockdev --getsize64 "$DISK")"
DISK_MODEL="$(lsblk -dno MODEL "$DISK" | sed 's/[[:space:]]*$//')"
# Partitionssuffix: nvme0n1 -> nvme0n1p1, sda -> sda1
case "$DISK_NAME" in *[0-9]) PSEP=p ;; *) PSEP="" ;; esac
P1="${DISK}${PSEP}1"; P2="${DISK}${PSEP}2"; P3="${DISK}${PSEP}3"

log "############################################################"
log "# ZIELPLATTE WIRD VOLLSTAENDIG UEBERSCHRIEBEN:"
log "#   Geraet : ${DISK}"
log "#   Modell : ${DISK_MODEL}"
log "#   Groesse: $((DISK_SIZE_B/1024/1024/1024)) GiB (${DISK_SIZE_B} B)"
log "#   Partitionen danach: ${P1} ESP · ${P2} /boot · ${P3} LVM"
log "############################################################"
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT "$DISK" || true

# Nichts von der Zielplatte darf gemountet sein (Live-Medium!).
# SICHERHEITSKRITISCH und deshalb fail-closed aufgebaut: `findmnt | grep -q`
# haette unter pipefail beim TREFFER (grep -q schliesst die Pipe sofort,
# findmnt kann SIGPIPE bekommen) einen Nicht-Null-Status liefern — die
# if-Bedingung waere falsch und der Guard haette die Platte freigegeben,
# obwohl sie gemountet ist. Genau der Fall, in dem er greifen muss.
# Deshalb: Liste erst vollstaendig holen (Fehler dabei = Abbruch, nicht
# "unauffaellig"), danach im Speicher suchen.
MOUNT_SRCS="$(findmnt -rno SOURCE)" \
  || fail "findmnt" "Mount-Liste nicht ermittelbar — der Schutz vor dem Ueberschreiben einer benutzten Platte ist nicht auswertbar"
if grep -q "^${DISK}" <<<"$MOUNT_SRCS"; then
  fail "diskbusy" "Auf ${DISK} liegt ein gemountetes Dateisystem — das ist nicht die richtige Platte."
fi
# Untergrenze ist die Original-Disk-Groesse aus dem Manifest, NICHT
# LV-Groesse + Reserve: vgcfgrestore repliziert das PV/VG/LV-Layout aus der
# Metadatensicherung exakt, das LV umfasst also bereits (Disk - ESP - /boot -
# Overhead) der Originalplatte — "LV + Reserve" zaehlte diese Reserve doppelt.
# Auf einer zur Originalplatte identischen Zielplatte (der Normalfall) war
# das rechnerisch unerfuellbar: LV_SIZE_B + 4 GiB > DISK_SIZE_B, obwohl
# Ziel- und Originalplatte exakt gleich gross sind. Bei Gleichheit besteht
# der Check.
ORIG_DISK_SIZE_B="$(md disk.size_bytes)"
if [ "$DISK_SIZE_B" -lt "$ORIG_DISK_SIZE_B" ]; then
  fail "disksmall" "Zielplatte kleiner als Original: ${DISK_SIZE_B} B < ${ORIG_DISK_SIZE_B} B (Original-Disk-Groesse aus dem Manifest)"
fi

# ----------------------------------------------------------------------------
# 4) GPT neu anlegen — Layout und PARTUUIDs wie im Original
# ----------------------------------------------------------------------------
log "Deaktiviere evtl. aktive Volume Groups auf der Zielplatte…"
vgchange -an "$VG_NAME" >/dev/null 2>&1 || true
swapoff -a 2>/dev/null || true

log "Schreibe GPT neu…"
wipefs -a "$DISK" >/dev/null 2>&1 || true
sgdisk --zap-all "$DISK" >/dev/null || fail "sgdiskzap" "sgdisk --zap-all fehlgeschlagen"
# 1G EF00 (ESP) · 2G 8300 (/boot) · Rest 8E00 (LVM). PARTUUIDs aus den
# Metadaten, damit auch Verweise per /dev/disk/by-partuuid gueltig bleiben.
sgdisk \
  -n 1:0:+1G   -t 1:EF00 -c 1:"EFI System Partition" -u "1:${ESP_PARTUUID}" \
  -n 2:0:+2G   -t 2:8300 -c 2:"boot"                 -u "2:${BOOT_PARTUUID}" \
  -n 3:0:0     -t 3:8E00 -c 3:"lvm"                  -u "3:${PV_PARTUUID}" \
  "$DISK" >/dev/null || fail "sgdisknew" "sgdisk-Partitionierung fehlgeschlagen"
partprobe "$DISK" || true
udevadm settle || true
sleep 2
for p in "$P1" "$P2" "$P3"; do
  [ -b "$p" ] || fail "nopart" "Partition ${p} ist nach partprobe nicht da"
done
log "Partitionen: $(lsblk -no NAME,SIZE,PARTUUID "$DISK" | tr '\n' ' ')"

# ----------------------------------------------------------------------------
# 5) Dateisysteme mit ERZWUNGENEN UUIDs
#    Ohne das waeren /etc/fstab und die GRUB-Konfiguration aus dem Image
#    ungueltig — genau deshalb kommen die UUIDs aus den Metadaten.
# ----------------------------------------------------------------------------
log "mkfs.vfat auf ${P1} (Volume-ID ${ESP_VOLID})…"
mkfs.vfat -F 32 -i "$ESP_VOLID" -n ESP "$P1" >/dev/null || fail "mkfsesp" "mkfs.vfat fehlgeschlagen"
log "mkfs.ext4 auf ${P2} (UUID ${BOOT_UUID})…"
mkfs.ext4 -F -q -U "$BOOT_UUID" "$P2" || fail "mkfsboot" "mkfs.ext4 (/boot) fehlgeschlagen"

# --- LVM ---------------------------------------------------------------------
# Der fstab-Eintrag fuer / verweist auf die dm-uuid, die sich aus VG-UUID und
# LV-UUID zusammensetzt (LVM-<VG-UUID ohne Bindestriche><LV-UUID ohne
# Bindestriche>). Ein frisches vgcreate/lvcreate wuerde neue UUIDs vergeben und
# das wiederhergestellte System faende sein Root nicht mehr. Deshalb ist der
# Hauptweg: PV mit Original-UUID anlegen und die komplette VG-Metadaten-
# sicherung einspielen (vgcfgrestore). Fallback nur, wenn das scheitert — dann
# wird die fstab-Zeile nach dem Entpacken auf UUID= umgeschrieben.
FSTAB_FIXUP=0
log "Stelle LVM her (PV/VG/LV mit Original-UUIDs)…"
if pvcreate --yes -ff --uuid "$PV_UUID" --restorefile "${META_DIR}/${PREFIX}-lvm.vg" "$P3" >/dev/null 2>&1 \
   && vgcfgrestore -f "${META_DIR}/${PREFIX}-lvm.vg" "$VG_NAME" >/dev/null 2>&1 \
   && vgchange -ay "$VG_NAME" >/dev/null 2>&1; then
  log "  vgcfgrestore erfolgreich."
else
  warn "vgcfgrestore-Weg fehlgeschlagen — Fallback auf frisches pvcreate/vgcreate/lvcreate."
  vgchange -an "$VG_NAME" >/dev/null 2>&1 || true
  wipefs -a "$P3" >/dev/null 2>&1 || true
  pvcreate --yes -ff "$P3" >/dev/null      || fail "pvcreate" "pvcreate fehlgeschlagen"
  vgcreate "$VG_NAME" "$P3" >/dev/null     || fail "vgcreate" "vgcreate fehlgeschlagen"
  lvcreate --yes -l 100%FREE -n "$LV_NAME" "$VG_NAME" >/dev/null || fail "lvcreate" "lvcreate fehlgeschlagen"
  FSTAB_FIXUP=1
fi
udevadm settle || true
LV_DEV="/dev/${VG_NAME}/${LV_NAME}"
[ -b "$LV_DEV" ] || fail "nolv" "LV ${LV_DEV} ist nicht da"

DM_UUID_NOW="$(dmsetup info -c --noheadings -o uuid "${VG_NAME//-/--}-${LV_NAME//-/--}" 2>/dev/null || echo '')"
if [ "$DM_UUID_NOW" = "$DM_UUID_EXPECTED" ]; then
  log "  dm-uuid stimmt mit dem Image ueberein — /etc/fstab bleibt unveraendert gueltig."
else
  warn "dm-uuid weicht ab (ist ${DM_UUID_NOW:-leer}, erwartet ${DM_UUID_EXPECTED}) — fstab wird nach dem Entpacken auf UUID= umgeschrieben."
  FSTAB_FIXUP=1
fi

log "mkfs.ext4 auf ${LV_DEV} (UUID ${ROOT_UUID})…"
mkfs.ext4 -F -q -U "$ROOT_UUID" "$LV_DEV" || fail "mkfsroot" "mkfs.ext4 (/) fehlgeschlagen"

post_stage restore-partitioned

# ----------------------------------------------------------------------------
# 6) Mounten und Archive laden
# ----------------------------------------------------------------------------
mkdir -p "$TARGET"
mount "$LV_DEV" "$TARGET" || fail "mountroot" "mount / fehlgeschlagen"
mkdir -p "$DL_DIR"

log "Lade Archive nach ${DL_DIR} …"
cp "${META_DIR}/SHA256SUMS" "${META_DIR}/${PREFIX}.json" \
   "${META_DIR}/${PREFIX}-gpt.bin" "${META_DIR}/${PREFIX}-lvm.vg" "$DL_DIR/"
cd "$DL_DIR"
for f in "${PREFIX}-root.tar.zst" "${PREFIX}-boot.tar.zst" "${PREFIX}-efi.tar.zst"; do
  log "  ${f}"
  curl -fsS --retry 3 --retry-connrefused -o "$f" "${SRC}/${f}" \
    || fail "archdl" "Download fehlgeschlagen: ${SRC}/${f}"
done
log "Pruefe alle Pruefsummen (sha256sum -c SHA256SUMS)…"
sha256sum -c SHA256SUMS >/dev/null || fail "archhash" "Pruefsummen der Archive stimmen nicht"
log "  alle Pruefsummen ok."
post_stage restore-downloaded

# ----------------------------------------------------------------------------
# 7) Entpacken — root, dann boot, dann efi
#    Die leeren Mountpoints (/boot, /boot/efi, /proc, /sys, /dev, /run, /tmp,
#    /var/tmp) stecken bereits in den Archiven (capture.sh legt sie per
#    find -xdev/-prune ausdruecklich hinein).
#    /var/tmp/restore ueberlebt das Entpacken des Root-Archivs: darin steht nur
#    der leere Verzeichniseintrag ./var/tmp, keine Loeschanweisung.
# ----------------------------------------------------------------------------
log "Entpacke Root-Archiv nach ${TARGET} …"
tar -I zstd -xf "${DL_DIR}/${PREFIX}-root.tar.zst" -C "$TARGET" \
    --xattrs --xattrs-include='*' --acls --numeric-owner \
  || fail "extractroot" "Entpacken des Root-Archivs fehlgeschlagen"

log "Mounte ${P2} nach ${TARGET}/boot und entpacke /boot-Archiv…"
mkdir -p "${TARGET}/boot"
mount "$P2" "${TARGET}/boot" || fail "mountboot" "mount /boot fehlgeschlagen"
tar -I zstd -xf "${DL_DIR}/${PREFIX}-boot.tar.zst" -C "${TARGET}/boot" \
    --xattrs --xattrs-include='*' --acls --numeric-owner \
  || fail "extractboot" "Entpacken des /boot-Archivs fehlgeschlagen"
[ -f "${TARGET}/boot/autoinstall/vmlinuz" ] && [ -f "${TARGET}/boot/autoinstall/initrd" ] \
  || fail "noautoinstall" "/boot/autoinstall/{vmlinuz,initrd} fehlen im Image — der Reprovision-Eintrag waere wirkungslos"

log "Mounte ${P1} nach ${TARGET}/boot/efi und entpacke ESP-Archiv…"
mkdir -p "${TARGET}/boot/efi"
mount "$P1" "${TARGET}/boot/efi" || fail "mountefi" "mount /boot/efi fehlgeschlagen"
# vfat kennt weder Eigentuemer noch Unix-Rechte; ein Restore mit --same-owner
# scheitert dort. Deshalb hier bewusst ohne — der ESP-Inhalt wird ohnehin
# gleich von grub-install neu geschrieben, das Archiv ist die Rueckfallebene.
tar -I zstd -xf "${DL_DIR}/${PREFIX}-efi.tar.zst" -C "${TARGET}/boot/efi" \
    --numeric-owner --no-same-owner --no-same-permissions \
  || fail "extractefi" "Entpacken des ESP-Archivs fehlgeschlagen"

post_stage restore-extracted

# ----------------------------------------------------------------------------
# 8) Swapdatei neu anlegen (war vom Root-Archiv ausgeschlossen)
# ----------------------------------------------------------------------------
if [ -n "$SWAP_PATH" ] && [ "$SWAP_SIZE_B" -gt 0 ]; then
  log "Lege Swapdatei ${SWAP_PATH} ($((SWAP_SIZE_B/1024/1024/1024)) GiB) neu an…"
  if fallocate -l "$SWAP_SIZE_B" "${TARGET}${SWAP_PATH}" 2>/dev/null \
     || dd if=/dev/zero of="${TARGET}${SWAP_PATH}" bs=1M count=$((SWAP_SIZE_B/1024/1024)) status=none; then
    chmod 600 "${TARGET}${SWAP_PATH}"
    mkswap "${TARGET}${SWAP_PATH}" >/dev/null || warn "mkswap fehlgeschlagen — System bootet trotzdem, swap.target bleibt degraded"
  else
    warn "Swapdatei konnte nicht angelegt werden — System bootet trotzdem, swap.target bleibt degraded"
  fi
fi

# ----------------------------------------------------------------------------
# 9) fstab-Reparatur, falls die dm-uuid nicht reproduziert werden konnte
# ----------------------------------------------------------------------------
if [ "$FSTAB_FIXUP" -eq 1 ]; then
  warn "Schreibe die Root-Zeile in ${TARGET}/etc/fstab auf UUID=${ROOT_UUID} um."
  cp "${TARGET}/etc/fstab" "${TARGET}/etc/fstab.golden-orig"
  awk -v u="UUID=${ROOT_UUID}" '
    $0 !~ /^#/ && $2=="/" { $1=u; print; next } { print }' \
    "${TARGET}/etc/fstab.golden-orig" > "${TARGET}/etc/fstab"
  log "  Original liegt als /etc/fstab.golden-orig daneben."
fi

# ----------------------------------------------------------------------------
# 10) chroot vorbereiten
# ----------------------------------------------------------------------------
log "Bereite chroot vor…"
for d in dev dev/pts proc sys run; do
  mkdir -p "${TARGET}/${d}"
  mount --bind "/${d}" "${TARGET}/${d}" || fail "bind" "mount --bind /${d} fehlgeschlagen"
done

# resolv.conf: im Zielsystem ist das ueblicherweise ein Symlink nach
# ../run/systemd/resolve/stub-resolv.conf. Der zeigt im chroot auf das
# gebindete /run der Live-Umgebung und laeuft dort ins Leere. Deshalb den
# Symlink kurz beiseitelegen und danach exakt wiederherstellen.
RESOLV_LINK=""
if [ -L "${TARGET}/etc/resolv.conf" ]; then
  RESOLV_LINK="$(readlink "${TARGET}/etc/resolv.conf")"
  rm -f "${TARGET}/etc/resolv.conf"
elif [ -f "${TARGET}/etc/resolv.conf" ]; then
  mv "${TARGET}/etc/resolv.conf" "${TARGET}/etc/resolv.conf.golden-orig"
fi
cp /etc/resolv.conf "${TARGET}/etc/resolv.conf" 2>/dev/null || true

restore_resolv() {
  rm -f "${TARGET}/etc/resolv.conf"
  if [ -n "$RESOLV_LINK" ]; then
    ln -s "$RESOLV_LINK" "${TARGET}/etc/resolv.conf"
  elif [ -f "${TARGET}/etc/resolv.conf.golden-orig" ]; then
    mv "${TARGET}/etc/resolv.conf.golden-orig" "${TARGET}/etc/resolv.conf"
  fi
}

# ----------------------------------------------------------------------------
# 11) Bootloader
# ----------------------------------------------------------------------------
log "grub-install im chroot…"
chroot "$TARGET" grub-install --target=x86_64-efi --efi-directory=/boot/efi \
       --bootloader-id=ubuntu --recheck \
  || fail "grubinstall" "grub-install fehlgeschlagen"
# Zusaetzlich der Wechselmedien-Pfad /EFI/BOOT/BOOTX64.EFI. Kostet nichts und
# rettet den Boot, falls die NVRAM-Variable nach dem Plattenwipe leer bleibt —
# bei einer Fernwiederherstellung ist das der Unterschied zwischen "bootet" und
# "physischer Eingriff noetig".
chroot "$TARGET" grub-install --target=x86_64-efi --efi-directory=/boot/efi \
       --bootloader-id=ubuntu --removable \
  || warn "grub-install --removable fehlgeschlagen (Fallback-Pfad fehlt)"

# ----------------------------------------------------------------------------
# 12) Permanenter Reprovision-Eintrag im Zielsystem
#     Zeigt auf denselben Installer-Kernel (/autoinstall/… relativ zur
#     /boot-Partition) und auf den RESTORE-Seed unter /golden/ — nicht auf
#     /autoinstall/, wo der Erstinstallations-Seed liegt.
#     Der Eintrag ist ein Sicherheitsnetz, kein Standard-Boot: GRUB_TIMEOUT=0
#     und GRUB_TIMEOUT_STYLE=hidden lassen ihn im Alltag unsichtbar; ausgeloest
#     wird er mit `grub-reboot helix-v5-reprovision` (oder von Hand im Menue).
# ----------------------------------------------------------------------------
log "Schreibe permanenten GRUB-Eintrag 'helix-v5-reprovision'…"
GRUB_CUSTOM="${TARGET}/etc/grub.d/40_custom"
MARK_BEGIN="# >>> helix-v5-reprovision BEGIN >>>"
MARK_END="# <<< helix-v5-reprovision END <<<"
if [ ! -f "$GRUB_CUSTOM" ]; then
  printf '#!/bin/sh\nexec tail -n +3 $0\n' > "$GRUB_CUSTOM"
fi
if grep -qF "$MARK_BEGIN" "$GRUB_CUSTOM"; then
  sed -i "\|^${MARK_BEGIN}\$|,\|^${MARK_END}\$|d" "$GRUB_CUSTOM"
fi
cat >> "$GRUB_CUSTOM" <<EOF
${MARK_BEGIN}
# Von golden/restore.sh gesetzt (${VERSION}). Startet den casper-Live-Installer
# und laesst dessen autoinstall-early-commands golden/restore.sh ausfuehren —
# also eine erneute Wiederherstellung aus dem Golden Image.
menuentry "helix-v5-reprovision" {
    insmod part_gpt
    insmod ext2
    search --no-floppy --set=root --fs-uuid ${BOOT_UUID}
    linux /autoinstall/vmlinuz autoinstall ip=dhcp url=${BOOT_ISO_URL} "ds=nocloud-net;s=${SEED_BASE}" ---
    initrd /autoinstall/initrd
}
${MARK_END}
EOF
chmod +x "$GRUB_CUSTOM"

log "update-grub im chroot…"
chroot "$TARGET" update-grub || fail "updategrub" "update-grub fehlgeschlagen"
grep -q 'helix-v5-reprovision' "${TARGET}/boot/grub/grub.cfg" \
  || fail "nomenuentry" "helix-v5-reprovision steht nicht in der erzeugten grub.cfg"
log "  helix-v5-reprovision ist in der grub.cfg."

log "Pruefe EFI-Booteintrag…"
# Auch hier erst einlesen, dann pruefen (`… | grep -qi` = dieselbe
# SIGPIPE-Falle; Folge waere nur eine falsche Warnung, aber die Regel gilt
# im ganzen Skript einheitlich). `|| true`: efibootmgr darf fehlen/scheitern,
# die Antwort ist dann schlicht "kein Eintrag sichtbar".
EFI_ENTRIES="$(efibootmgr -v 2>/dev/null || true)"
if grep -qi 'ubuntu' <<<"$EFI_ENTRIES"; then
  efibootmgr 2>/dev/null | sed 's/^/  /' || true
else
  warn "kein EFI-Booteintrag 'ubuntu' sichtbar — der Wechselmedien-Pfad"
  warn "  /EFI/BOOT/BOOTX64.EFI (grub-install --removable) ist die Rueckfallebene."
fi

restore_resolv
post_stage restore-grub-done

# ----------------------------------------------------------------------------
# 13) Aufraeumen und Reboot
# ----------------------------------------------------------------------------
log "Loesche Downloads unter ${DL_DIR} …"
rm -rf "$DL_DIR"

log "Haenge aus…"
for d in run sys proc dev/pts dev; do
  umount -l "${TARGET}/${d}" 2>/dev/null || true
done
umount "${TARGET}/boot/efi" || warn "umount /boot/efi fehlgeschlagen"
umount "${TARGET}/boot"     || warn "umount /boot fehlgeschlagen"
sync
umount "$TARGET"            || warn "umount / fehlgeschlagen"
vgchange -an "$VG_NAME" >/dev/null 2>&1 || true
sync

log "Wiederherstellung abgeschlossen — Reboot in das wiederhergestellte System."
trap - EXIT
reboot
