"""Onglet « Utilisation » de la GUI V6 (G4 — alignement maquette).

Reprend la structure de ``docs/ui_mockup_v6.html`` : carte Apparence (sélecteur
de thème), carte Documents (dropzone + liste), carte Format, barre d'actions,
carte progression (étapes), carte résultats (cartes statistiques). La logique
(runner moteur câblé, threading, file d'événements, arrêt coopératif, anti
double-lancement) est conservée. Aucune logique de détection ici.
"""

from __future__ import annotations

import queue
import threading
from pathlib import Path
from tkinter import filedialog

import customtkinter as ctk

from gui_v6 import theme as theme_mod
from gui_v6 import ui_kit
from gui_v6.fsutil import open_in_file_manager
from gui_v6.processing_runner import ProcessingRunner, default_output_dir

_STEPS = ["📖 Extraction", "🧠 Détection", "🔒 Masquage", "📄 PDF final"]

_HELP_USAGE = (
    "Anonymiser vos documents.\n\n"
    "1) Choisissez un fichier ou un dossier de documents.\n"
    "2) Vérifiez le format de sortie.\n"
    "3) Cliquez sur « Lancer » : l'application détecte et masque les données "
    "personnelles, puis écrit les documents anonymisés dans un dossier de sortie.\n\n"
    "Tout le traitement se fait 100 % en local sur ce poste. Aucun document "
    "n'est envoyé sur Internet."
)


def failure_hint(summary, output_dir) -> str | None:
    """Message localisant les documents livrés, ou None si run nominal.

    Honnête : les documents en échec / quarantaine ne sont PAS anonymisés et
    ne sont donc pas écrits. Si AUCUN document n'a abouti, on ne prétend pas
    qu'un dossier de sortie contient des documents anonymisés.
    """
    if summary is None or output_dir is None:
        return None
    if summary.failed == 0 and not getattr(summary, "stopped", False):
        return None
    if summary.succeeded == 0:
        return (
            "Aucun document n'a été anonymisé. Les documents en échec ou en "
            "quarantaine ne sont PAS anonymisés et n'ont pas été écrits."
        )
    return (
        f"Documents anonymisés écrits dans : {output_dir}\n"
        "Les documents en échec ou en quarantaine ne sont PAS anonymisés et "
        "n'ont pas été écrits."
    )


class UsageTab(ctk.CTkFrame):
    def __init__(
        self,
        master,
        runner: ProcessingRunner | None = None,
        config_provider=None,
        config_path: Path | None = None,
        palette: dict | None = None,
        on_theme_change=None,
        current_theme: str = theme_mod.DEFAULT_THEME,
        usage_reporter=None,
        diag_reporter=None,
        **kwargs,
    ):
        self._p = palette or theme_mod.get_palette(current_theme)
        super().__init__(master, fg_color=self._p["bg"], **kwargs)
        self._runner = runner or ProcessingRunner()
        self._config_provider = config_provider
        self._config_path = config_path
        self._on_theme_change = on_theme_change
        self._current_theme = current_theme
        # Callback(summary) appelé en fin de run pour la télémétrie d'usage
        # (envoi non bloquant, injecté par l'app avec le contexte licence).
        self._usage_reporter = usage_reporter
        # Callback(summary) appelé en fin de run pour les diagnostics RGPD
        # (envoi non bloquant, injecté par l'app avec le contexte licence).
        self._diag_reporter = diag_reporter

        self._input_path: Path | None = None
        self._output_dir: Path | None = None
        self._last_output_dir: Path | None = None
        self._stop_event: threading.Event | None = None
        self._is_running = False
        self._events: "queue.Queue[tuple]" = queue.Queue()

        self._build()
        self.after(120, self._drain_events)

    # -- construction UI --------------------------------------------------

    def _build(self) -> None:
        p = self._p

        # Bandeau d'introduction + aide « ? »
        intro = ctk.CTkFrame(self, fg_color="transparent")
        intro.pack(fill="x", padx=14, pady=(12, 0))
        ctk.CTkLabel(
            intro,
            text="Sélectionnez vos documents puis lancez l'anonymisation (100 % local).",
            text_color=p["text_dim"],
            font=ui_kit.font(12),
            anchor="w",
        ).pack(side="left", padx=(2, 6))
        ui_kit.help_button(intro, p, _HELP_USAGE, title="Comment ça marche ?").pack(side="right", padx=2)

        # Carte Apparence (sélecteur de thème)
        appearance = ui_kit.Card(self, p, title="🎨 Apparence")
        appearance.pack(fill="x", padx=14, pady=(14, 7))
        row = ctk.CTkFrame(appearance, fg_color="transparent")
        row.pack(fill="x", padx=16, pady=(0, 14))
        for name in theme_mod.theme_names():
            ui_kit.pill_button(
                row,
                p,
                theme_mod.THEME_LABELS.get(name, name),
                command=lambda n=name: self._change_theme(n),
                active=(name == self._current_theme),
            ).pack(side="left", padx=(0, 8))

        # Carte Documents + dropzone
        docs = ui_kit.Card(self, p, title="📁 Documents à anonymiser")
        docs.pack(fill="x", padx=14, pady=7)
        dz = ctk.CTkFrame(
            docs, fg_color=p["divider"], border_color=p["card_border"], border_width=2, corner_radius=8
        )
        dz.pack(fill="x", padx=16, pady=(0, 8))
        # Zone réellement cliquable (ouvre le sélecteur de fichier). Le DnD natif
        # est différé ; on ne laisse pas une métaphore « déposer » trompeuse.
        dz.configure(cursor="hand2")
        dz.bind("<Button-1>", lambda _e: self._pick_file())
        for _txt, _color, _size, _pady in (
            ("⬆️", p["text"], 30, (20, 4)),
            ("Cliquez pour choisir un fichier", p["text"], 14, 0),
            ("PDF · Word · Images · Texte", p["text_muted"], 12, (2, 10)),
        ):
            _lbl = ctk.CTkLabel(dz, text=_txt, text_color=_color, font=ui_kit.font(_size))
            _lbl.configure(cursor="hand2")
            _lbl.bind("<Button-1>", lambda _e: self._pick_file())
            _lbl.pack(pady=_pady)
        acts = ctk.CTkFrame(dz, fg_color="transparent")
        acts.pack(pady=(0, 20))
        ui_kit.secondary_button(acts, p, "📄 Fichier", command=self._pick_file).pack(side="left", padx=4)
        ui_kit.secondary_button(acts, p, "📁 Dossier entier", command=self._pick_folder).pack(side="left", padx=4)
        self._target_label = ctk.CTkLabel(docs, text="Aucune source sélectionnée", text_color=p["text_muted"], font=ui_kit.font(12), anchor="w")
        self._target_label.pack(fill="x", padx=16, pady=(0, 14))

        # Carte Format de sortie
        fmt = ui_kit.Card(self, p, title="💾 Format de sortie")
        fmt.pack(fill="x", padx=14, pady=7)
        grid = ctk.CTkFrame(fmt, fg_color="transparent")
        grid.pack(fill="x", padx=16, pady=(0, 14))
        for icon, name, sub in [("📄", "PDF anonymisé", "Zones noircies"), ("📝", "Texte .txt", "Mots → [NOM]…")]:
            fcard = ctk.CTkFrame(grid, fg_color=p["card"], border_color=p["primary"], border_width=2, corner_radius=8)
            fcard.pack(side="left", expand=True, fill="x", padx=4)
            ctk.CTkLabel(fcard, text=icon, font=ui_kit.font(20)).pack(pady=(10, 0))
            ctk.CTkLabel(fcard, text=name, text_color=p["text"], font=ui_kit.font(13, "bold")).pack()
            ctk.CTkLabel(fcard, text=sub, text_color=p["text_muted"], font=ui_kit.font(11)).pack(pady=(0, 10))

        # Barre d'actions
        brow = ctk.CTkFrame(self, fg_color="transparent")
        brow.pack(fill="x", padx=14, pady=7)
        ui_kit.secondary_button(brow, p, "✖ Effacer", command=self._clear_input).pack(side="right", padx=(8, 0))
        self._run_btn = ui_kit.primary_button(brow, p, "▶ Lancer l'anonymisation", command=self._start, large=True)
        self._run_btn.configure(state="disabled")
        self._run_btn.pack(side="right")

        # Carte progression
        self._psec = ui_kit.Card(self, p, title="⏳ Traitement en cours")
        self._progress = ctk.CTkProgressBar(self._psec, progress_color=p["primary"])
        self._progress.set(0.0)
        self._progress.pack(fill="x", padx=16, pady=(0, 6))
        self._status_label = ctk.CTkLabel(self._psec, text="Prêt.", text_color=p["text_muted"], font=ui_kit.font(12), anchor="w")
        self._status_label.pack(fill="x", padx=16)
        steps = ctk.CTkFrame(self._psec, fg_color="transparent")
        steps.pack(fill="x", padx=16, pady=6)
        for s in _STEPS:
            ctk.CTkLabel(steps, text=s, fg_color=p["divider"], text_color=p["text_muted"], corner_radius=99, font=ui_kit.font(11)).pack(side="left", padx=3, ipadx=6, ipady=2)
        self._log = ctk.CTkTextbox(self._psec, height=110, fg_color=p["divider"], text_color=p["text_dim"], font=ui_kit.font(11), border_color=p["card_border"], border_width=1)
        self._log.pack(fill="x", padx=16, pady=8)
        self._log.configure(state="disabled")
        self._stop_btn = ui_kit.secondary_button(self._psec, p, "⏹ Arrêter", command=self._request_stop)
        self._stop_btn.configure(state="disabled")
        self._stop_btn.pack(anchor="e", padx=16, pady=(0, 14))
        # masquée tant qu'aucun run

        # Carte résultats (cartes statistiques)
        self._rsec = ui_kit.Card(self, p, title="✅ Résultats")
        self._stats_row = ctk.CTkFrame(self._rsec, fg_color="transparent")
        self._stats_row.pack(fill="x", padx=16, pady=(0, 14))
        self._result_built = False
        self._hint_row = None

    # -- thème ------------------------------------------------------------

    def _change_theme(self, name: str) -> None:
        if self._on_theme_change is not None:
            self._on_theme_change(name)

    # -- runner câblé -----------------------------------------------------

    def _build_run_runner(self) -> tuple[ProcessingRunner, Path | None]:
        if self._config_provider is None:
            return self._runner, self._output_dir
        from gui_v6.engine_bridge import make_process_fn

        cfg = self._config_provider()
        settings = cfg.to_engine_settings(self._config_path)
        runner = ProcessingRunner(process_fn=make_process_fn(settings))
        output_dir = self._output_dir or getattr(cfg, "output_dir", None)
        return runner, output_dir

    # -- sélection --------------------------------------------------------

    def _pick_file(self) -> None:
        path = filedialog.askopenfilename(title="Choisir un document")
        if path:
            self._set_input(Path(path))

    def _pick_folder(self) -> None:
        path = filedialog.askdirectory(title="Choisir un dossier")
        if path:
            self._set_input(Path(path))

    def _clear_input(self) -> None:
        self._input_path = None
        self._target_label.configure(text="Aucune source sélectionnée")
        self._run_btn.configure(state="disabled")

    def _set_input(self, path: Path) -> None:
        self._input_path = path
        count = len(self._runner.discover(path))
        if path.is_dir():
            self._target_label.configure(text=f"📁 {path}  ·  {count} document(s) détecté(s)")
        else:
            self._target_label.configure(text=f"📄 {path.name}  ·  sortie {default_output_dir(path)}")
        self._run_btn.configure(state="normal" if count > 0 else "disabled")

    # -- exécution --------------------------------------------------------

    def _start(self) -> None:
        if self._input_path is None or self._is_running:
            return
        self._is_running = True
        run_runner, run_output_dir = self._build_run_runner()
        self._last_output_dir = run_output_dir or default_output_dir(self._input_path)
        self._stop_event = threading.Event()
        self._run_btn.configure(state="disabled")
        self._psec.pack(fill="x", padx=14, pady=7)
        self._stop_btn.configure(state="normal")
        self._progress.set(0.0)
        self._clear_log()
        self._set_status("Traitement en cours…")

        input_path, output_dir, stop = self._input_path, run_output_dir, self._stop_event

        def work() -> None:
            try:
                summary = run_runner.run(
                    input_path,
                    output_dir,
                    on_progress=lambda done, total, name: self._events.put(("progress", done, total, name)),
                    on_log=lambda msg: self._events.put(("log", msg)),
                    stop_event=stop,
                )
                self._events.put(("done", summary))
            except Exception as exc:  # garde-fou : ne jamais laisser le thread tuer l'UI
                self._events.put(("error", str(exc)))

        self._worker = threading.Thread(target=work, daemon=True)
        self._worker.start()

    def _request_stop(self) -> None:
        if self._stop_event is not None:
            self._stop_event.set()
            self._set_status("Arrêt demandé…")
            self._stop_btn.configure(state="disabled")

    # -- file d'événements worker → UI ------------------------------------

    def _drain_events(self) -> None:
        try:
            while True:
                event = self._events.get_nowait()
                self._handle_event(event)
        except queue.Empty:
            pass
        self.after(120, self._drain_events)

    def _handle_event(self, event: tuple) -> None:
        kind = event[0]
        if kind == "progress":
            _, done, total, name = event
            self._progress.set(done / total if total else 0.0)
            self._set_status(f"{done}/{total} — {name}")
        elif kind == "log":
            self._append_log(event[1])
        elif kind == "done":
            self._finish(event[1])
        elif kind == "error":
            self._append_log(f"Erreur : {event[1]}")
            self._finish(None)

    def _finish(self, summary) -> None:
        self._is_running = False
        self._stop_btn.configure(state="disabled")
        self._run_btn.configure(state="normal")
        if summary is None:
            self._set_status("Terminé avec erreur.")
            return
        if summary.stopped:
            self._set_status(f"Arrêté : {summary.succeeded}/{summary.total} traités.")
        else:
            self._progress.set(1.0)
            self._set_status(f"Terminé : {summary.succeeded} OK, {summary.failed} échec(s) sur {summary.total}.")
        self._show_results(summary)
        self._show_failure_hint(summary)
        self._send_usage_telemetry(summary)
        self._send_diagnostics(summary)

    def _send_usage_telemetry(self, summary) -> None:
        """Envoie la télémétrie d'usage en fin de run, sans bloquer l'UI ni le run."""
        reporter = self._usage_reporter
        if reporter is None:
            return

        def work():
            try:
                reporter(summary)
            except Exception:
                pass  # un échec de télémétrie ne doit jamais remonter

        threading.Thread(target=work, daemon=True).start()

    def _send_diagnostics(self, summary) -> None:
        """Envoie les diagnostics en fin de run, sans bloquer l'UI ni le run."""
        reporter = self._diag_reporter
        if reporter is None:
            return

        def work():
            try:
                reporter(summary)
            except Exception:
                pass  # un échec diagnostic ne doit jamais remonter

        threading.Thread(target=work, daemon=True).start()

    def _show_results(self, summary) -> None:
        p = self._p
        for w in self._stats_row.winfo_children():
            w.destroy()
        cards = [
            (str(summary.total), "Documents", p["primary"]),
            (str(summary.succeeded), "Réussis", p["success"]),
            (str(summary.failed), "Échecs", p["danger"] if summary.failed else p["text_muted"]),
            ("OK" if summary.ok else "KO", "Statut", p["success"] if summary.ok else p["warning"]),
        ]
        for value, label, color in cards:
            ui_kit.StatCard(self._stats_row, p, value, label, value_color=color).pack(side="left", expand=True, fill="x", padx=4)
        self._rsec.pack(fill="x", padx=14, pady=(7, 14))

    def _show_failure_hint(self, summary) -> None:
        # Nettoyage inconditionnel : éviter l'empilement et les hints périmés.
        if getattr(self, "_hint_row", None) is not None:
            self._hint_row.destroy()
            self._hint_row = None
        hint = failure_hint(summary, getattr(self, "_last_output_dir", None))
        if hint is None:
            return
        p = self._p
        self._hint_row = ctk.CTkFrame(self._rsec, fg_color="transparent")
        self._hint_row.pack(fill="x", padx=16, pady=(0, 12))
        ctk.CTkLabel(
            self._hint_row, text=hint, text_color=p["text_dim"], font=ui_kit.font(11),
            anchor="w", justify="left",
        ).pack(side="left", fill="x", expand=True)
        # Bouton « ouvrir » uniquement s'il y a réellement des documents livrés.
        if summary is not None and summary.succeeded > 0:
            ui_kit.secondary_button(
                self._hint_row, p, "📂 Ouvrir le dossier",
                command=lambda: open_in_file_manager(self._last_output_dir),
            ).pack(side="right")

    # -- helpers widgets --------------------------------------------------

    def _set_status(self, text: str) -> None:
        self._status_label.configure(text=text)

    def _clear_log(self) -> None:
        self._log.configure(state="normal")
        self._log.delete("1.0", "end")
        self._log.configure(state="disabled")

    def _append_log(self, message: str) -> None:
        self._log.configure(state="normal")
        self._log.insert("end", message + "\n")
        self._log.see("end")
        self._log.configure(state="disabled")
