#!/usr/bin/env python3
# FWS — application GRAPHIQUE de réparation du système (mode « Réparer FWS »
# du live). GTK3 : le live embarque python-gobject/gtk3 via Anaconda.
#
# Pilote le MOTEUR fws-recovery en sous-commandes :
#   fws-recovery scan                    → détecte les installations FWS
#   fws-recovery repair <action> <dev>   → répare (sortie streamée ici)
#   fws-recovery shell <dev>             → terminal chroot (xterm)
#
# Deux contextes de lancement :
#   • mode « Réparer FWS » (live OU entrée GRUB de l'OS installé) : session
#     root via fws-recovery-session → le moteur est appelé directement ;
#   • menu d'applications de l'OS installé : lancée en UTILISATEUR → chaque
#     appel du moteur passe par pkexec (politique org.fws.recovery,
#     auth_admin_keep : un seul mot de passe pour toute la session).

import os
import shutil
import subprocess
import sys

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk, Pango  # noqa: E402

GLib.set_prgname("fws-recovery-gui")

_ENGINE = "/usr/local/bin/fws-recovery"


def _engine_cmd(args):
    """Commande moteur, élevée via pkexec si on n'est pas root."""
    cmd = [_ENGINE] + args
    if os.geteuid() != 0:
        cmd = ["pkexec"] + cmd
    return cmd

_ACTIONS = [
    ("grub", "Réinstaller GRUB",
     "le système ne démarre plus / « Operating System not found »"),
    ("initramfs", "Régénérer l'initramfs",
     "kernel panic au démarrage, module manquant (mkinitcpio -P)"),
    ("pacman", "Réparer pacman",
     "verrou bloqué, clés de signature cassées"),
    ("pacman-update", "Réparer pacman + mise à jour complète",
     "idem, puis pacman -Syu (réseau requis)"),
    ("all", "Tout réparer",
     "initramfs puis GRUB — le réflexe « ça ne boote plus »"),
]


class RecoveryWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="FWS — Réparation du système")
        self.set_icon_name("fws-recovery")
        self.set_default_size(720, 560)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_border_width(18)
        self.connect("destroy", Gtk.main_quit)

        self._proc = None
        self._installs = []          # [(dev, nom), …]
        self._action_buttons = []

        root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        self.add(root)

        title = Gtk.Label()
        title.set_markup("<span size='x-large' weight='bold'>Réparation du système FWS</span>")
        root.pack_start(title, False, False, 0)

        self._status = Gtk.Label(label="Recherche des installations FWS sur les disques…")
        self._status.set_xalign(0)
        root.pack_start(self._status, False, False, 0)

        # Sélecteur d'installation (rempli après le scan).
        picker_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        picker_row.pack_start(Gtk.Label(label="Système à réparer :"), False, False, 0)
        self._picker = Gtk.ComboBoxText()
        picker_row.pack_start(self._picker, True, True, 0)
        root.pack_start(picker_row, False, False, 0)

        # Boutons d'action (style fws-hello : titre + description).
        for key, label, desc in _ACTIONS:
            button = Gtk.Button()
            inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
            lab = Gtk.Label()
            lab.set_markup(f"<b>{label}</b>")
            lab.set_xalign(0)
            sub = Gtk.Label(label=desc)
            sub.set_xalign(0)
            sub.get_style_context().add_class("dim-label")
            inner.pack_start(lab, False, False, 0)
            inner.pack_start(sub, False, False, 0)
            button.add(inner)
            button.connect("clicked", self._on_action, key)
            root.pack_start(button, False, False, 0)
            self._action_buttons.append(button)

        shell_btn = Gtk.Button(label="Ouvrir un terminal dans le système (chroot)")
        shell_btn.connect("clicked", self._on_shell)
        root.pack_start(shell_btn, False, False, 0)
        self._action_buttons.append(shell_btn)

        # Journal (sortie du moteur, en continu).
        frame = Gtk.Frame(label="Journal")
        scroll = Gtk.ScrolledWindow()
        scroll.set_min_content_height(150)
        self._log = Gtk.TextView()
        self._log.set_editable(False)
        self._log.set_cursor_visible(False)
        self._log.set_monospace(True)
        self._log.modify_font(Pango.FontDescription("Monospace 9"))
        scroll.add(self._log)
        frame.add(scroll)
        root.pack_start(frame, True, True, 0)

        # Pied : redémarrer / éteindre.
        footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        reboot = Gtk.Button(label="Redémarrer")
        reboot.connect("clicked", lambda _b: self._power("reboot"))
        poweroff = Gtk.Button(label="Éteindre")
        poweroff.connect("clicked", lambda _b: self._power("poweroff"))
        footer.pack_end(reboot, False, False, 0)
        footer.pack_end(poweroff, False, False, 0)
        root.pack_start(footer, False, False, 0)

        self._set_busy(True)
        GLib.idle_add(self._start_scan)

    # ── Utilitaires ─────────────────────────────────────────────────────────
    def _append(self, text):
        # Rebranding d'affichage : les outils sous-jacents parlent
        # d'« Arch Linux » — pas le journal FWS. (Les identifiants techniques
        # comme archlinux-keyring restent tels quels : ce sont de vrais noms
        # de fichiers/paquets, les masquer fausserait le diagnostic.)
        text = text.replace("Arch Linux", "FWS").replace("Fedora", "FWS")
        buf = self._log.get_buffer()
        buf.insert(buf.get_end_iter(), text)
        mark = buf.create_mark(None, buf.get_end_iter(), False)
        self._log.scroll_mark_onscreen(mark)
        buf.delete_mark(mark)

    def _set_busy(self, busy):
        for b in self._action_buttons:
            b.set_sensitive(not busy and bool(self._installs))
        # Sensible même avec UNE seule install : un combo grisé donne
        # l'impression que TOUTE l'app est désactivée (retour utilisateur).
        self._picker.set_sensitive(not busy and bool(self._installs))

    def _selected_dev(self):
        idx = self._picker.get_active()
        if 0 <= idx < len(self._installs):
            return self._installs[idx][0]
        return None

    def _power(self, what):
        subprocess.run(["systemctl", what], check=False)

    # ── Scan des installations ──────────────────────────────────────────────
    def _start_scan(self):
        self._run_engine(["scan"], self._scan_done, quiet=True)
        return False

    def _scan_done(self, rc, output):
        self._installs = []
        for line in output.splitlines():
            if "|" in line:
                dev, _, name = line.partition("|")
                self._installs.append((dev.strip(), name.strip()))
                self._picker.append_text(f"{name.strip()}  —  {dev.strip()}")
        if self._installs:
            self._picker.set_active(0)
            n = len(self._installs)
            self._status.set_markup(
                f"<b>{n}</b> installation{'s' if n > 1 else ''} FWS détectée{'s' if n > 1 else ''}."
                " Choisis une réparation :")
        elif rc in (126, 127):
            # pkexec : authentification annulée ou refusée.
            self._status.set_markup(
                "<b>Authentification annulée.</b> Ferme puis relance"
                " l'application pour réessayer.")
        else:
            self._status.set_markup(
                "<b>Aucune installation FWS détectée.</b> Vérifie les disques"
                " (terminal ci-dessous) ou redémarre.")
            self._append("Aucune installation FWS trouvée.\n\n"
                         + subprocess.run(["lsblk", "-o", "NAME,SIZE,TYPE,FSTYPE"],
                                          capture_output=True, text=True,
                                          check=False).stdout)
        self._set_busy(False)
        # Le terminal reste utile même sans installation détectée (diagnostic).
        self._action_buttons[-1].set_sensitive(True)

    # ── Exécution du moteur avec sortie en continu ──────────────────────────
    def _run_engine(self, args, done_cb, quiet=False):
        self._set_busy(True)
        self._proc = subprocess.Popen(
            _engine_cmd(args), stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT, text=True, bufsize=1)
        chunks = []

        def on_io(source, _cond):
            line = source.readline()
            if line:
                chunks.append(line)
                if not quiet:
                    self._append(line)
                return True
            rc = self._proc.wait()
            done_cb(rc, "".join(chunks))
            return False

        GLib.io_add_watch(self._proc.stdout,
                          GLib.IO_IN | GLib.IO_HUP, on_io)

    def _on_action(self, _button, key):
        dev = self._selected_dev()
        if not dev:
            return
        label = next(lab for k, lab, _d in _ACTIONS if k == key)
        self._status.set_markup(f"<b>{label}</b> sur {dev} — en cours…")
        self._append(f"\n───── {label} ({dev}) ─────\n")
        self._run_engine(["repair", key, dev], self._action_done)

    def _action_done(self, rc, _output):
        if rc == 0:
            self._status.set_markup("<b>Terminé ✔</b> — autre réparation, ou redémarre.")
        elif rc in (126, 127):
            self._status.set_markup(
                "<b>Authentification annulée</b> — rien n'a été modifié.")
        else:
            self._status.set_markup(
                f"<b>Échec (code {rc})</b> — détail dans le journal ci-dessous.")
        self._set_busy(False)

    def _on_shell(self, _button):
        dev = self._selected_dev()
        if dev and os.geteuid() == 0:
            subprocess.Popen([_ENGINE, "shell", dev])
        elif dev:
            # pkexec purge DISPLAY → le moteur ouvre le chroot DANS cet xterm
            # (lancé en utilisateur, donc visible sur X11 comme Wayland).
            subprocess.Popen(["xterm", "-title",
                              "FWS — terminal dans le système (chroot)",
                              "-e", "pkexec", _ENGINE, "shell", dev])
        elif shutil.which("xterm"):
            subprocess.Popen(["xterm", "-title", "FWS — terminal"])


def main():
    if not Gtk.init_check()[0]:
        # Pas d'affichage : le .zprofile retombera sur l'assistant console.
        return 1
    if os.geteuid() != 0 and not shutil.which("pkexec"):
        dlg = Gtk.MessageDialog(message_type=Gtk.MessageType.ERROR,
                                buttons=Gtk.ButtonsType.CLOSE,
                                text="pkexec introuvable : lance fws-recovery-gui en root.")
        dlg.run()
        return 1
    win = RecoveryWindow()
    win.show_all()
    Gtk.main()
    return 0


if __name__ == "__main__":
    sys.exit(main())
