#!/bin/bash
# ============================================================
#  fws-gameboot — orchestrateur dual-boot « hibernate-swap » (FWS ⇄ Windows)
#
#  But : jouer à un jeu à anticheat kernel (Valorant/Vanguard) qui REFUSE la
#  virtualisation, sans quitter durablement FWS. On hiberne FWS (RAM → swap), on
#  arme un boot UEFI one-shot (BootNext) vers un Windows 11 BARE-METAL, et on
#  redémarre. Windows tourne sur le vrai matériel (AUCUN hyperviseur) → Vanguard
#  content. Au retour de Windows, le noyau reprend la session FWS via resume=.
#
#  Ce N'EST PAS une VM et il n'y a AUCUN hyperviseur : c'est un dual-boot
#  orchestré. Le « seamless » vient de l'hibernation (session restaurée), pas
#  d'une exécution simultanée (impossible sans hyperviseur = ban Vanguard).
#
#  Filet de sûreté PERMANENT : BootOrder[0] = FWS (posé par fws-gameboot-bootfix
#  à chaque démarrage). BootNext ne sert QUE pour les transitions one-shot ; tout
#  crash/reboot non géré retombe sur FWS.
#
#  ⚠ Exige une VALIDATION MATÉRIELLE réelle (hibernation + resume GPU NVIDIA,
#    sémantique BootNext du firmware). Voir docs/hibernate-swap-dualboot.md §7.
# ============================================================
set -u

CONF=/etc/fws/gameboot.conf
# shellcheck source=/dev/null
[ -r "$CONF" ] && . "$CONF"
: "${FWS_WINDOWS_BOOTNUM:=}"
: "${FWS_HIBERNATE_FALLBACK:=reboot}"
: "${FWS_SWAP_MARGIN_MB:=2048}"
: "${FWS_DEFAULT_GAME:=valorant}"

log()  { printf '\e[36m[gameboot]\e[0m %s\n' "$*" >&2; }
warn() { printf '\e[33m[gameboot] %s\e[0m\n' "$*" >&2; }
die()  { printf '\e[31m[gameboot] ERREUR : %s\e[0m\n' "$*" >&2; exit 1; }

# Notifie l'utilisateur graphique (best-effort ; on tourne en root via pkexec).
notify_user() {
    command -v notify-send >/dev/null 2>&1 || return 0
    local u uid
    while IFS=: read -r u _ uid _; do
        [ "$uid" -ge 1000 ] && [ "$uid" -le 60000 ] || continue
        [ -S "/run/user/$uid/bus" ] || continue
        sudo -u "$u" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
            notify-send -a FWS -u critical "FWS Gaming" "$1" 2>/dev/null || true
    done < /etc/passwd
}

need_root() { [ "$(id -u)" -eq 0 ] || die "root requis — lance : pkexec fws-gameboot ${1:-}"; }

# --- Entrée UEFI « Windows Boot Manager » (numéro Boot####, HEX MAJUSCULE) ---
find_windows_bootnum() {
    local n
    if [ -n "$FWS_WINDOWS_BOOTNUM" ]; then n="$FWS_WINDOWS_BOOTNUM"; else
        n="$(efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *Windows Boot Manager.*/\1/p' | head -1)"
    fi
    # Normaliser en MAJUSCULE : efibootmgr affiche « BootNext: 000A » en majuscule ;
    # une valeur config en minuscule ferait échouer la relecture (m6).
    printf '%s' "${n^^}"
}
# --- Notre propre entrée (FWS ou, à défaut, GRUB) ---
find_fws_bootnum() {
    efibootmgr 2>/dev/null | sed -n 's/^Boot\([0-9A-Fa-f]\{4\}\)\*\? *\(FWS\|GRUB\).*/\1/p' | head -1
}

# --- VRAM en usage (Mo), 0 si pas de NVIDIA — recopiée dans l'image S4 ---
vram_used_mb() {
    command -v nvidia-smi >/dev/null 2>&1 || { echo 0; return; }
    nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null \
        | awk '{s+=$1} END{print s+0}'
}

# Renvoie 0 si tout est OK pour hiberner. Décrit chaque échec sur stderr.
preflight() {
    local mem_kb swap_kb vram_mb need_mb have_mb
    grep -q disk /sys/power/state 2>/dev/null || { warn "hibernation indisponible (/sys/power/state sans « disk »)"; return 1; }
    grep -q 'resume=' /proc/cmdline || { warn "resume= absent de la cmdline noyau (hibernation non reprenable)"; return 1; }
    swap_kb=$(awk '/^SwapTotal:/{print $2}' /proc/meminfo)
    [ "${swap_kb:-0}" -gt 0 ] || { warn "aucun swap actif (rien où écrire l'image)"; return 1; }
    mem_kb=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
    vram_mb=$(vram_used_mb)
    need_mb=$(( mem_kb/1024 + vram_mb + FWS_SWAP_MARGIN_MB ))
    have_mb=$(( swap_kb/1024 ))
    if [ "$have_mb" -lt "$need_mb" ]; then
        warn "swap trop petit : ${have_mb} Mo < ${need_mb} Mo requis (RAM $((mem_kb/1024)) + VRAM ${vram_mb} + marge ${FWS_SWAP_MARGIN_MB})"
        return 1
    fi
    return 0
}

# Résout le point de montage de l'ESP (FAT). Vide si aucun.
find_esp() {
    findmnt -rno TARGET /boot/efi 2>/dev/null \
        || findmnt -rno TARGET /efi 2>/dev/null \
        || findmnt -rno TARGET /boot 2>/dev/null
}

cmd_doctor() {
    local sb f w
    sb=$(bootctl status 2>/dev/null | sed -n 's/^ *Secure Boot: *//p' | head -1)
    f="$(find_fws_bootnum)"; w="$(find_windows_bootnum)"
    echo "== fws-gameboot doctor =="
    echo "Entrée FWS       : ${f:-?}"
    echo "Entrée Windows   : ${w:-INTROUVABLE}"
    echo "RAM              : $(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo)/1024 )) Mo"
    echo "Swap             : $(( $(awk '/^SwapTotal:/{print $2}' /proc/meminfo)/1024 )) Mo"
    echo "VRAM en usage    : $(vram_used_mb) Mo"
    echo "resume= cmdline  : $(grep -o 'resume=[^ ]*' /proc/cmdline || echo 'ABSENT')"
    echo "Secure Boot      : ${sb:-inconnu}"
    echo -n "Préflight        : "
    if preflight 2>/dev/null; then printf '\e[32mOK\e[0m\n'; else printf '\e[31mÉCHEC\e[0m\n'; preflight; fi
    case "$sb" in
        enabled) ;;
        *) warn "Secure Boot inactif → Vanguard refusera Windows (VAN9003). Configure d'abord : sudo fws-secureboot-setup" ;;
    esac
}

cmd_to_windows() {
    need_root "to-windows"
    local win esp tok bootnext_set="" rc m game="${FWS_DEFAULT_GAME:-valorant}" game_req=""
    while [ $# -gt 0 ]; do
        case "$1" in
            --game)   game="${2:-}"; game_req=1; shift 2 ;;
            --game=*) game="${1#--game=}"; game_req=1; shift ;;
            *) die "argument inconnu : $1 (usage : to-windows [--game <id>])" ;;
        esac
    done
    # Charte d'identifiant alignée sur les clés de games.json : [a-z0-9_-]
    # (l'underscore doit passer, sinon divergence FWS↔Windows).
    game="$(printf '%s' "$game" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')"
    [ -n "$game_req" ] && [ -z "$game" ] && warn "identifiant --game invalide après nettoyage — jeu par défaut appliqué."
    win="$(find_windows_bootnum)"
    [ -n "$win" ] || die "entrée « Windows Boot Manager » introuvable (Windows installé + entrée UEFI présente ?)"
    esp="$(find_esp)"

    # Jeton « quel jeu lancer », lu ET CONSOMMÉ par fws-play côté Windows.
    # Écriture ATOMIQUE (.tmp puis mv) sur l'ESP FAT uniquement. En cas d'échec,
    # on PURGE tout jeton périmé (jamais de rejeu d'un ancien choix) → Windows
    # lancera le jeu par défaut plutôt qu'un mauvais jeu.
    if [ -n "$esp" ] && [ "$(findmnt -no FSTYPE "$esp" 2>/dev/null)" = vfat ]; then
        if [ -n "$game" ]; then
            mkdir -p "$esp/EFI/FWS" 2>/dev/null || true
            tok="$esp/EFI/FWS/launch.json"
            if printf '{"game":"%s"}\n' "$game" > "$tok.tmp" 2>/dev/null && mv -f "$tok.tmp" "$tok" 2>/dev/null; then
                log "Jeu demandé : $game"
            else
                rm -f "$tok" "$tok.tmp" 2>/dev/null || true
                warn "écriture du jeton échouée → jeton purgé (Windows lancera le jeu par défaut)."
                notify_user "Choix du jeu non transmis — Windows lancera le jeu par défaut."
            fi
        fi
    elif [ -n "$game_req" ]; then
        warn "ESP FAT introuvable → le choix du jeu ne peut pas être transmis à Windows."
    fi

    # Filet : ne JAMAIS laisser BootNext armé si on ne bascule pas réellement
    # (sinon départ surprise vers Windows au prochain reboot manuel).
    cleanup() { [ -n "$bootnext_set" ] && { efibootmgr -N >/dev/null 2>&1 || true; log "BootNext annulé (nettoyage)"; }; }
    trap cleanup EXIT

    # Démonter toute NTFS montée : jamais rw persistante à travers le cycle.
    # `findmnt -lno` (liste, PAS -r/raw) → chemins NON échappés (espaces OK) ; on
    # se limite aux vrais types NTFS (pas fuseblk générique).
    while IFS= read -r m; do
        [ -n "$m" ] && umount "$m" 2>/dev/null || true
    done < <(findmnt -lno TARGET -t ntfs,ntfs3 2>/dev/null)

    if ! preflight; then
        case "$FWS_HIBERNATE_FALLBACK" in
            reboot)
                warn "Préflight hibernation KO → bascule en REBOOT SIMPLE (session FWS NON préservée)."
                notify_user "Hibernation indisponible — bascule vers Windows par reboot simple (session non sauvegardée)."
                efibootmgr -n "$win" >/dev/null || die "écriture de BootNext échouée"
                bootnext_set=1   # ARMÉ : si la relecture échoue, le trap nettoie (M2)
                efibootmgr 2>/dev/null | grep -qi "BootNext: $win" || die "BootNext non confirmé par le firmware"
                trap - EXIT      # bascule assumée
                log "Reboot simple vers Windows (Boot$win)…"
                exec systemctl reboot
                ;;
            *) die "Préflight hibernation KO et fallback='abort' → on reste sur FWS." ;;
        esac
    fi

    # --- Bascule NOMINALE : BootNext=Windows puis hibernation ---
    efibootmgr -n "$win" >/dev/null || die "écriture de BootNext échouée"
    bootnext_set=1
    # RELECTURE obligatoire : certains firmwares ignorent/écrasent BootNext.
    efibootmgr 2>/dev/null | grep -qi "BootNext: $win" \
        || die "BootNext non pris en compte par le firmware (entrée Boot$win) — carte non compatible ?"
    log "BootNext armé vers Windows (Boot$win). Hibernation…"
    notify_user "Lancement de Windows pour le jeu — FWS s'endort, ta session sera restaurée au retour."

    # M3 : l'ESP (FAT, potentiellement partagée avec Windows) ne doit pas rester
    # montée rw à travers le cycle — un cache FAT périmé réécrit au resume
    # corromprait les bootloaders (seul chemin réaliste vers un vrai brick). On
    # la remonte en lecture seule le temps de l'hibernation, rw au retour.
    [ -n "$esp" ] && { mount -o remount,ro "$esp" 2>/dev/null || true; }

    # NB : on n'enveloppe PAS « systemctl hibernate » dans un timeout. Un
    # hibernate RÉUSSI ne « rend la main » qu'au RESUME (après la session
    # Windows, potentiellement > 1 h) : un timeout tuerait les hibernations
    # réussies. Une hibernation qui HANG (driver refusant S4) laisse la machine
    # allumée et n'est PAS mitigeable en userspace (le process est gelé) ; c'est
    # un risque à valider sur matériel réel (docs §7 #6). Le préflight a déjà
    # éliminé les causes courantes (swap absent/petit, resume= manquant).
    if systemctl hibernate; then
        # On revient ici au RESUME. Bascule réussie : le firmware a normalement
        # consommé BootNext au boot de Windows — MAIS un override manuel (menu
        # F12) peut le laisser armé. On le purge donc INCONDITIONNELLEMENT (M1) :
        # no-op s'il est déjà consommé, rétablit le filet BootOrder=FWS sinon.
        [ -n "$esp" ] && { mount -o remount,rw "$esp" 2>/dev/null || true; }
        efibootmgr -N >/dev/null 2>&1 || true
        bootnext_set=""
        trap - EXIT
        log "Session FWS restaurée. Bon retour."
    else
        rc=$?
        [ -n "$esp" ] && { mount -o remount,rw "$esp" 2>/dev/null || true; }
        warn "Hibernation échouée (rc=$rc)."
        if [ "$FWS_HIBERNATE_FALLBACK" = reboot ]; then
            notify_user "Hibernation impossible — bascule vers Windows par reboot simple (session non sauvegardée)."
            if efibootmgr -n "$win" >/dev/null 2>&1 && efibootmgr 2>/dev/null | grep -qi "BootNext: $win"; then
                bootnext_set=""    # on assume la bascule, pas de nettoyage
                trap - EXIT
                log "Reboot simple vers Windows…"
                exec systemctl reboot
            fi
        fi
        die "Bascule annulée — on reste sur FWS (BootNext nettoyé)."
    fi
}

usage() {
    cat <<EOF
fws-gameboot — dual-boot hibernate-swap (FWS ⇄ Windows bare-metal, anticheat kernel)

  fws-gameboot doctor               Vérifie l'environnement (préflight, boot)
  fws-gameboot to-windows [--game <id>]  Hiberne FWS, bascule + lance <id> (root/pkexec)
  fws-gameboot status               Alias de « doctor »

Le jeu par défaut (FWS_DEFAULT_GAME) est « valorant ». Les identifiants connus
sont définis dans games.json côté Windows (valorant, lol, …).

Config : $CONF
Détails et risques : docs/hibernate-swap-dualboot.md
EOF
}

case "${1:-}" in
    to-windows)     shift; cmd_to_windows "$@" ;;
    doctor|status)  cmd_doctor ;;
    ''|-h|--help|help) usage ;;
    *) die "sous-commande inconnue : $1 (voir --help)" ;;
esac
