# FWS — Page « Récapitulatif » : écran À PART ENTIÈRE avant l'installation.
#
# POURQUOI ICI (airootfs, pas dans le paquet anaconda) : même mécanisme que la
# spoke « Desktop Environment » (fws_desktop.py) — Anaconda découvre les spokes
# au RUNTIME (collect) dans ce dossier → l'overlay airootfs suffit, AUCUN
# rebuild du paquet anaconda. Le glade associé est dans
# /usr/share/anaconda/ui/spokes/fws_recap.glade (aussi via l'airootfs).
#
# PLACEMENT DANS LE FLUX : standalone spoke « postForHub = SummaryHub » avec
# priority = -10. L'ordre des écrans est pre(SummaryHub) → SummaryHub →
# post(SummaryHub) trié par priority croissante (UserInterface.
# _orderActionClasses). ProgressSpoke — l'écran qui DÉMARRE l'installation
# dans son refresh() — est aussi post(SummaryHub), priority 0 par défaut →
# cette page s'intercale ENTRE les deux : l'utilisateur clique « Commencer
# l'installation » sur le hub, voit CE récapitulatif (le disque n'a encore
# subi AUCUNE écriture), et l'installation ne démarre qu'à son clic
# « Lancer l'installation ». Le bouton « Précédent » ramène au hub pour
# modifier les choix (cf. _on_back_clicked : la file d'actions du GUI est
# consommée à chaque écran, on y réinsère le hub).
#
# CONTENU : configuration choisie par l'utilisateur (via les modules D-Bus
# d'Anaconda : langue, clavier, fuseau, comptes, partitionnement + les
# fichiers /tmp des spokes FWS : bureau, hostname) ET matériel détecté
# (virtualisation, GPU, disques, réseau) — mêmes sources que ce que le
# %post du kickstart et fws-setup-hardware appliqueront.
#
# ⚠ « completed » doit rester False : _instantiateAction SAUTE les standalone
#   spokes complétées (mécanisme prévu pour les installs kickstart).
# ⚠ Chemin couplé à la version Python d'Arch (3.14) — comme fws_desktop.py —
#   et le bouton « Précédent » s'appuie sur les internes du GUI d'anaconda
#   45.8 (_actions/_currentAction) : à re-vérifier à chaque bump d'anaconda.

import inspect

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core import util
from pyanaconda.modules.common.constants.objects import DISK_SELECTION
from pyanaconda.modules.common.constants.services import (
    LOCALIZATION,
    STORAGE,
    TIMEZONE,
    USERS,
)
from pyanaconda.modules.common.structures.user import UserData
from pyanaconda.ui.gui import GraphicalUserInterface
from pyanaconda.ui.gui.hubs.summary import SummaryHub
from pyanaconda.ui.gui.spokes import StandaloneSpoke

log = get_module_logger(__name__)

__all__ = ["FWSRecapSpoke"]

# Doit rester aligné sur le %post du kickstart (étape « Bureau choisi ») et
# sur les libellés de la spoke fws_desktop.
_DESKTOP_LABELS = {
    "cli": "ligne de commande uniquement (aucun bureau)",
    "gnome": "GNOME (+ Firefox, GDM)",
    "kde": "KDE Plasma (+ Firefox, SDDM)",
    "hyprland": "Hyprland — Wayland, non compatible VR (+ Firefox, SDDM)",
    "i3": "i3 — X11, compatible VR/SteamVR (+ Firefox, SDDM)",
    "xfce": "XFCE (+ Firefox, SDDM)",
}

_PARTITIONING_LABELS = {
    "AUTOMATIC": "automatique (recommandé)",
    "INTERACTIVE": "personnalisé (assistant)",
    "CUSTOM": "personnalisé",
    "BLIVET": "avancé (blivet-gui)",
    "MANUAL": "points de montage manuels",
}


def _safe(getter, default):
    """Valeur d'un getter D-Bus/système, ou défaut : le récap ne doit JAMAIS
    faire tomber l'installateur, quel que soit le module indisponible (d'où
    le except large : dasbus lève des types variés selon la panne)."""
    try:
        return getter()
    except Exception:  # pylint: disable=broad-except
        return default


def _capture(*argv):
    """stdout d'une commande, chaîne vide si absente ou en échec."""
    try:
        return util.execWithCapture(argv[0], list(argv[1:])) or ""
    except (OSError, RuntimeError):
        return ""


def _read_flag(path):
    try:
        with open(path) as fh:
            return fh.read().strip()
    except OSError:
        return ""


def _config_lines():
    """Section « configuration choisie » : interroge les modules D-Bus
    d'Anaconda (remplis par les spokes) + les fichiers /tmp des spokes FWS."""
    lines = []

    lang = _safe(lambda: LOCALIZATION.get_proxy().Language, "")
    if lang:
        lines.append(f"  • Langue          : {lang}")

    layouts = _safe(lambda: list(LOCALIZATION.get_proxy().XLayouts), [])
    if layouts:
        lines.append(f"  • Clavier (X11)   : {', '.join(layouts)}")

    tz = _safe(lambda: TIMEZONE.get_proxy().Timezone, "")
    if tz:
        ntp = _safe(lambda: TIMEZONE.get_proxy().NTPEnabled, None)
        suffix = {True: " — synchronisation NTP activée",
                  False: " — synchronisation NTP désactivée"}.get(ntp, "")
        lines.append(f"  • Fuseau horaire  : {tz}{suffix}")

    users = _safe(
        lambda: UserData.from_structure_list(USERS.get_proxy().Users), [])
    if users:
        names = ", ".join(u.name for u in users if u.name)
        lines.append(f"  • Utilisateur(s)  : {names} (sudo via wheel)")
    else:
        lines.append("  • Utilisateur(s)  : AUCUN — pensez-y avant de lancer !")

    root_locked = _safe(lambda: USERS.get_proxy().IsRootAccountLocked, None)
    root_pw = _safe(lambda: USERS.get_proxy().IsRootPasswordSet, None)
    if root_locked:
        root_txt = "verrouillé (recommandé)"
    elif root_pw:
        root_txt = "actif, mot de passe défini"
    elif root_pw is not None:
        # Même règle que l'étape « Sécurité » du %post : un root resté sans
        # mot de passe sera verrouillé en fin d'installation.
        root_txt = "sans mot de passe → sera VERROUILLÉ en fin d'install"
    else:
        root_txt = ""
    if root_txt:
        lines.append(f"  • Compte root     : {root_txt}")

    # Hostname : écrit par le spoke Utilisateur (patch user.py du paquet
    # anaconda). Même nettoyage / même défaut que le %post --nochroot.
    hn = "".join(c for c in _read_flag("/tmp/fws-hostname")
                 if c.isalnum() or c in "._-") or "fws"
    lines.append(f"  • Nom de machine  : {hn}")

    # Bureau : écrit par fws_desktop.apply() (sortie de la spoke). Absent =
    # spoke jamais visitée = « cli », exactement le défaut du %post.
    desk = _read_flag("/tmp/fws-desktop") or "cli"
    lines.append(f"  • Bureau          : {_DESKTOP_LABELS.get(desk, desk)}")

    method = _safe(
        lambda: STORAGE.get_proxy(
            STORAGE.get_proxy().AppliedPartitioning).PartitioningMethod, "")
    if method:
        lines.append("  • Partitionnement : "
                     f"{_PARTITIONING_LABELS.get(method, method)}")

    return lines, desk, method


def _build_summary():
    config_lines, desk, method = _config_lines()
    config_text = "\n".join(config_lines) \
        or "  (configuration non disponible)"

    # Disques ciblés par l'installation (spoke « Destination »).
    selected = set(_safe(
        lambda: list(STORAGE.get_proxy(DISK_SELECTION).SelectedDisks), []))
    disks = []
    for line in _capture("lsblk", "-dno", "NAME,SIZE,MODEL,TYPE").splitlines():
        fields = line.split()
        if fields and fields[-1] == "disk":
            mark = "  ← CIBLE de l'installation" if fields[0] in selected else ""
            disks.append("  • " + " ".join(fields[:-1]) + mark)
    disk_text = "\n".join(disks) or "  (aucun disque détecté)"

    if method == "AUTOMATIC":
        disk_warn = (
            "  ⚠ Rien n'a encore été écrit. Le partitionnement automatique\n"
            "  EFFACERA ENTIÈREMENT le(s) disque(s) cible(s) au lancement.")
    else:
        disk_warn = (
            "  ⚠ Rien n'a encore été écrit. Au lancement, ce sont vos choix\n"
            "  de partitionnement faits dans l'installateur qui s'appliqueront.")

    virt = (_capture("systemd-detect-virt").strip().splitlines() or ["none"])[0]
    if virt in ("", "none"):
        virt = "aucune (machine physique)"

    gpus = []
    for line in _capture("lspci").splitlines():
        low = line.lower()
        if ("vga" in low or "3d" in low or "display" in low) and ":" in line:
            gpus.append("  • " + line.split(":", 2)[-1].strip())
    gpu_text = "\n".join(gpus) or "  (aucune carte graphique détectée)"

    try:
        net_ok = util.execWithRedirect("ip", ["route", "get", "1.1.1.1"]) == 0
    except (OSError, RuntimeError):
        net_ok = False
    if net_ok:
        net = "disponible ✔"
    else:
        net = ("INDISPONIBLE ✖ — bureau, drivers et stack FWS ne\n"
               "                      s'installeront PAS (rattrapable au 1er boot)")

    extras = ("Steam, Wine, VR (Monado/OpenXR), winboat-bin"
              if desk != "cli" else "rien de plus (choix CLI)")

    return f"""\
══════════════════════════════════════════════════════════
        FWS — RÉCAPITULATIF AVANT INSTALLATION
══════════════════════════════════════════════════════════

CONFIGURATION CHOISIE
{config_text}

DISQUES
{disk_warn}
{disk_text}

MATÉRIEL DÉTECTÉ
  • Virtualisation  : {virt}
  • Réseau          : {net}
  Cartes graphiques (drivers installés automatiquement en fin d'install) :
{gpu_text}

DÉROULEMENT DE L'INSTALLATION
  1. Partitionnement, puis copie du système live FWS (payload LiveOS)
  2. Noyau + initramfs régénérés (mkinitcpio), GRUB installé
  3. Bureau choisi + navigateur installés (réseau requis)
  4. Matériel : drivers GPU, outils VM, son, Bluetooth, Podman
     + si bureau choisi : {extras}

« Précédent » pour revenir modifier vos choix —
« Lancer l'installation » pour démarrer réellement l'installation.
"""


class FWSRecapSpoke(StandaloneSpoke):
    """Récapitulatif des choix, affiché entre le hub et l'installation."""

    builderObjects = ["fwsRecapWindow"]
    mainWidgetName = "fwsRecapWindow"
    uiFile = "spokes/fws_recap.glade"

    postForHub = SummaryHub
    priority = -10

    @staticmethod
    def get_screen_id():
        """Identifiant unique de cet écran."""
        return "fws-recap"

    @property
    def completed(self):
        # False, sinon _instantiateAction saute l'écran (cf. en-tête).
        return False

    def apply(self):
        # Rien à persister : cette page ne fait que montrer l'état.
        pass

    def initialize(self):
        StandaloneSpoke.initialize(self)
        # Le bouton « continuer » de CETTE page est le vrai déclencheur :
        # l'écran suivant (ProgressSpoke) démarre la file d'installation.
        self.window.get_continue_button().set_label("_Lancer l'installation")

        # Capture pour « Précédent » : initialize() est appelé DEPUIS
        # GraphicalUserInterface._on_continue_clicked (clic « Commencer
        # l'installation » du hub), AVANT la bascule de _currentAction → à
        # cet instant ui._currentAction est ENCORE l'instance du hub Résumé,
        # et le « self » de l'appelant est l'UI. Aucune API publique ne les
        # expose ; on remonte la pile d'appels (déterministe — pas de scan
        # mémoire, pas de variable globale).
        self._gui = None
        self._hub = None
        frame = inspect.currentframe()
        try:
            frame = frame.f_back
            while frame is not None:
                candidate = frame.f_locals.get("self")
                if isinstance(candidate, GraphicalUserInterface):
                    self._gui = candidate
                    current = getattr(candidate, "_currentAction", None)
                    if isinstance(current, SummaryHub):
                        self._hub = current
                    break
                frame = frame.f_back
        finally:
            del frame

        # Bouton « Précédent », placé avec les boutons de navigation
        # (à côté de « Quitter » — leur parent est la GtkButtonBox du bas).
        quit_button = self.window.get_quit_button()
        button_box = quit_button.get_parent()
        self._back_button = Gtk.Button.new_with_mnemonic("_Précédent")
        self._back_button.connect("clicked", self._on_back_clicked)
        button_box.add(self._back_button)
        button_box.reorder_child(self._back_button, 0)
        if self._gui is None or self._hub is None:
            # Références introuvables (flux inattendu) : bouton grisé plutôt
            # qu'une navigation cassée — l'install reste possible.
            log.warning("FWS recap: hub/UI introuvables, « Précédent » désactivé.")
            self._back_button.set_sensitive(False)
        self._back_button.show()

    def _on_back_clicked(self, _button):
        ui = self._gui
        hub = self._hub
        if ui is None or hub is None:
            return
        log.info("FWS recap: retour au hub Résumé pour modification.")
        # La file d'actions du GUI est CONSOMMÉE (pop(0)) à chaque écran :
        # ici _actions = [FWSRecapSpoke, ProgressSpoke] et le hub en est déjà
        # sorti. On le réinsère en tête pour que le prochain « Commencer
        # l'installation » ré-enchaîne hub → NOUVELLE page récap (résumé
        # recalculé avec les choix modifiés) → installation.
        if not ui._actions or ui._actions[0] is not SummaryHub:
            ui._actions.insert(0, SummaryHub)
        # _on_continue_clicked a désactivé le « continuer » du hub en le
        # quittant : on le réarme, sinon « Commencer l'installation » est mort.
        hub.window.set_may_continue(True)
        # Même séquence que la transition officielle (_on_continue_clicked).
        self.exited.emit(self)
        hub.refresh()
        ui.mainWindow.setCurrentAction(hub)
        hub.entered.emit(hub)
        ui._currentAction = hub

    def refresh(self):
        StandaloneSpoke.refresh(self)
        text = _build_summary()
        self.builder.get_object("recapView").get_buffer().set_text(text)
        # Copie pour le harnais de debug du .xinitrc (fws-debug.txt).
        try:
            with open("/tmp/fws-hw-summary.txt", "w") as fh:
                fh.write(text)
        except OSError:
            pass
