#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Pseudonymisation – GUI v3 (UX simplifiée + infobulles + créateur de règle)
--------------------------------------------------------------------------
- Mode "Simple" par défaut (vocabulaire non-tech) + Mode "Avancé" (règles YAML)
- Options de sortie claires : "PDF anonymisé (léger)" et "PDF image (très sûr)" avec infobulles
- Gestion de dictionnaires YAML (whitelist/blacklist/overrides)
- Créateur de règle (Mot exact / Forme proche / Modèle avancé) avec prévisualisation
- Résumé par document (compte des remplacements) + bouton "Ouvrir dossier des résultats"
- Auto-fix YAML : conversion automatique des patterns en bloc littéral si le YAML est mal cité

Dépendances : tkinter, PyYAML, PyMuPDF, pdfplumber, pdfminer.six, Pillow
"""
from __future__ import annotations
import io
import json
import os
import platform
import re
import queue
import threading
from pathlib import Path
from typing import Dict, Any, List

import tkinter as tk
from tkinter import filedialog, messagebox, ttk

# Core anonymisation (laisse ce fichier à côté de ce script)
try:
    import anonymizer_core_refactored as core
except Exception as e:
    raise SystemExit(f"Impossible d'importer anonymizer_core_refactored: {e}")

try:
    import yaml
except Exception:
    yaml = None

from config_defaults import (
    RUNTIME_DICTIONARIES_CONFIG_PATH,
    read_default_dictionaries_text,
    read_runtime_dictionaries_overlay_text,
)

APP_TITLE = "Pseudonymisation de PDF"
DEFAULT_CFG = RUNTIME_DICTIONARIES_CONFIG_PATH

# YAML par défaut externalisé dans config/dictionnaires.default.yml
DEFAULTS_CFG_TEXT = read_default_dictionaries_text()
RUNTIME_CFG_TEXT = read_runtime_dictionaries_overlay_text()

# ---------- util : ToolTip & helpers ----------
class ToolTip:
    def __init__(self, widget, text: str):
        self.widget = widget
        self.text = text
        self.tip = None
        widget.bind("<Enter>", self.show)
        widget.bind("<Leave>", self.hide)
    def show(self, *_):
        if self.tip is not None: return
        x = self.widget.winfo_rootx() + 20
        y = self.widget.winfo_rooty() + self.widget.winfo_height() + 6
        self.tip = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(True)
        tw.wm_geometry(f"+{x}+{y}")
        lab = tk.Label(tw, text=self.text, justify=tk.LEFT, relief=tk.SOLID, borderwidth=1, padx=8, pady=6)
        lab.pack(ipadx=1)
    def hide(self, *_):
        if self.tip:
            self.tip.destroy(); self.tip=None

def open_folder(path: Path):
    try:
        if platform.system() == "Windows":
            os.startfile(str(path))  # type: ignore[attr-defined]
        elif platform.system() == "Darwin":
            os.system(f"open '{path}'")
        else:
            os.system(f"xdg-open '{path}'")
    except Exception:
        pass

# ---------- App ----------
class App:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title(APP_TITLE)
        self.root.geometry("1250x880")

        # Etat
        self.dir_var = tk.StringVar()
        self.status_var = tk.StringVar(value="Prêt.")
        self.cfg_path = tk.StringVar(value=str(DEFAULT_CFG))
        self.queue: "queue.Queue[str]" = queue.Queue()

        # Choix format
        self.format_var = tk.StringVar(value="vector")  # "vector" ou "raster"

        # Mémoire config
        self.cfg_data: Dict[str, Any] = {}

        # UI
        self._build_ui()
        self._pump_logs()

        # Prépare YAML
        self._ensure_cfg_exists()
        self._load_cfg()

    # ----- UI -----
    def _build_ui(self):
        wrap = tk.Frame(self.root, padx=10, pady=10)
        wrap.pack(fill=tk.BOTH, expand=True)

        # Tabs Simple / Avancé
        self.nb = ttk.Notebook(wrap)
        self.nb.pack(fill=tk.BOTH, expand=True)

        # --- Onglet Simple ---
        simple = tk.Frame(self.nb, padx=12, pady=12)
        self.nb.add(simple, text="Simple")

        row = tk.Frame(simple); row.pack(fill=tk.X)
        tk.Label(row, text="Vos documents :").pack(side=tk.LEFT)
        tk.Entry(row, textvariable=self.dir_var).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        tk.Button(row, text="Choisir…", command=self._browse).pack(side=tk.LEFT, padx=3)

        # Choix format clair
        fmt = tk.LabelFrame(simple, text="Format du document final")
        fmt.pack(fill=tk.X, pady=10)

        # PDF anonymisé (léger)
        rb_vec = tk.Radiobutton(fmt, text="PDF anonymisé (léger)", variable=self.format_var, value="vector")
        rb_vec.pack(anchor="w", padx=6, pady=2)
        ToolTip(rb_vec, "Supprime le texte et applique des boîtes noires.\nFichier léger. Le texte n’est plus lisible mais la sélection reste possible.")

        # PDF image (très sûr)
        rb_ras = tk.Radiobutton(fmt, text="PDF image (très sûr)", variable=self.format_var, value="raster")
        rb_ras.pack(anchor="w", padx=6, pady=2)
        ToolTip(rb_ras, "Convertit chaque page en image puis ajoute des boîtes noires.\nAucun texte résiduel. Fichier plus lourd et non sélectionnable.")

        # Boutons action
        actions = tk.Frame(simple); actions.pack(fill=tk.X, pady=(6,2))
        self.btn_run = tk.Button(actions, text="Anonymiser", command=self._run, height=1)
        self.btn_run.pack(side=tk.LEFT)
        tk.Button(actions, text="Aide (2 min)", command=self._show_help).pack(side=tk.LEFT, padx=6)
        self.btn_open_out = tk.Button(actions, text="Ouvrir le dossier de résultats", command=self._open_out, state=tk.DISABLED)
        self.btn_open_out.pack(side=tk.RIGHT)

        # Rapport
        tk.Label(simple, text="Rapport d’exécution :").pack(anchor="w")
        self.txt = tk.Text(simple, height=22)
        self.txt.pack(fill=tk.BOTH, expand=True, pady=(2,0))
        tk.Label(simple, textvariable=self.status_var, anchor="w").pack(fill=tk.X, pady=(4,0))

        # --- Onglet Avancé ---
        adv = tk.Frame(self.nb, padx=12, pady=12)
        self.nb.add(adv, text="Avancé")

        # Bloc dictionnaires YAML
        cfg = tk.LabelFrame(adv, text="Règles & dictionnaires (YAML)", padx=8, pady=8)
        cfg.pack(fill=tk.X, pady=6)
        tk.Label(cfg, text="Fichier YAML :").grid(row=0, column=0, sticky="w")
        tk.Entry(cfg, textvariable=self.cfg_path, width=60).grid(row=0, column=1, sticky="we", padx=6)
        tk.Button(cfg, text="Parcourir", command=self._cfg_browse).grid(row=0, column=2)
        tk.Button(cfg, text="Créer/Charger", command=self._load_cfg).grid(row=0, column=3, padx=4)
        tk.Button(cfg, text="Sauver", command=self._save_cfg).grid(row=0, column=4)
        tk.Button(cfg, text="Recharger", command=self._reload_cfg).grid(row=0, column=5, padx=4)
        tk.Button(cfg, text="Restaurer défauts", command=self._restore_defaults).grid(row=0, column=6)
        cfg.grid_columnconfigure(1, weight=1)
        ToolTip(cfg, "Les règles définissent ce qu’il faut masquer (blacklist), ce qu’il faut garder (whitelist) et les modèles personnalisés.")

        # Créateur de règle
        rc = tk.LabelFrame(adv, text="Créer rapidement une règle", padx=8, pady=8)
        rc.pack(fill=tk.X, pady=6)
        tk.Label(rc, text="Exemple (copiez/collez une ligne du PDF) :").grid(row=0, column=0, sticky="w")
        self.rule_example = tk.Entry(rc, width=80); self.rule_example.grid(row=0, column=1, columnspan=4, sticky="we", padx=6)
        tk.Label(rc, text="Type de modèle :").grid(row=1, column=0, sticky="e")
        self.rule_type = ttk.Combobox(rc, values=["Mot exact", "Forme proche", "Modèle avancé"], state="readonly"); self.rule_type.set("Mot exact")
        self.rule_type.grid(row=1, column=1, sticky="w")
        ToolTip(self.rule_type, "Mot exact : masque exactement ce que vous tapez.\nForme proche : tolère espaces/variantes.\nModèle avancé : expression régulière (pour experts).")
        tk.Label(rc, text="Remplacer par :").grid(row=1, column=2, sticky="e")
        self.rule_placeholder = tk.Entry(rc, width=18); self.rule_placeholder.insert(0, "[MASK]"); self.rule_placeholder.grid(row=1, column=3, sticky="w")
        tk.Label(rc, text="Où appliquer :").grid(row=1, column=4, sticky="e")
        self.rule_scope = ttk.Combobox(rc, values=["partout", "narratif", "tables_valeur", "entetes_pieds"], state="readonly"); self.rule_scope.set("partout"); self.rule_scope.grid(row=1, column=5, sticky="w")
        self.flag_ic = tk.BooleanVar(value=True); self.flag_bow = tk.BooleanVar(value=True)
        tk.Checkbutton(rc, text="Ignorer la casse (A=a)", variable=self.flag_ic).grid(row=2, column=1, sticky="w")
        tk.Checkbutton(rc, text="Respecter les mots entiers", variable=self.flag_bow).grid(row=2, column=2, sticky="w")
        tk.Button(rc, text="Prévisualiser", command=self._preview_rule).grid(row=2, column=4)
        tk.Button(rc, text="Enregistrer la règle", command=self._save_rule).grid(row=2, column=5)

    # ----- YAML helpers -----
    def _ensure_cfg_exists(self):
        p = Path(self.cfg_path.get())
        p.parent.mkdir(parents=True, exist_ok=True)
        if not p.exists():
            p.write_text(RUNTIME_CFG_TEXT, encoding="utf-8")

    def _cfg_browse(self):
        d = filedialog.asksaveasfilename(defaultextension=".yml", filetypes=[("YAML","*.yml *.yaml"), ("Tous","*.*")])
        if d:
            self.cfg_path.set(d)

    def _load_cfg(self):
        if yaml is None:
            messagebox.showerror("PyYAML manquant", "Installez PyYAML (pip install pyyaml).")
            return
        self._ensure_cfg_exists()
        try:
            with open(self.cfg_path.get(), "r", encoding="utf-8") as f:
                self.cfg_data = yaml.safe_load(f) or {}
            self._log(f"Règles chargées depuis : {self.cfg_path.get()}")
        except Exception as e:
            # Auto-fix : convertir pattern: "..." en bloc littéral
            try:
                raw = Path(self.cfg_path.get()).read_text(encoding="utf-8")
                fixed = re.sub(r"(^\s*pattern\s*:\s*)(\"[^\n]*\")", r"\1|-\n      \2", raw, flags=re.MULTILINE)
                if fixed != raw:
                    Path(self.cfg_path.get()).write_text(fixed, encoding="utf-8")
                    with open(self.cfg_path.get(), "r", encoding="utf-8") as f:
                        self.cfg_data = yaml.safe_load(f) or {}
                    self._log("Le fichier YAML contenait des guillemets problématiques. Correction automatique appliquée.")
                else:
                    raise
            except Exception as e2:
                messagebox.showerror("Fichier de règles invalide", f"Impossible de charger le YAML:\n{e}\n\nEssayez de restaurer les valeurs par défaut.")

    def _save_cfg(self):
        if yaml is None:
            messagebox.showerror("PyYAML manquant", "Installez PyYAML (pip install pyyaml).")
            return
        try:
            with open(self.cfg_path.get(), "w", encoding="utf-8") as f:
                yaml.safe_dump(self.cfg_data or {}, f, allow_unicode=True, sort_keys=False)
            self._log("Règles sauvegardées.")
        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible d'écrire le fichier de règles: {e}")

    def _reload_cfg(self):
        self._load_cfg(); self._log("Règles rechargées.")

    def _restore_defaults(self):
        try:
            Path(self.cfg_path.get()).write_text(RUNTIME_CFG_TEXT, encoding="utf-8")
            self._log("Surcharge locale réinitialisée.")
            self._load_cfg()
        except Exception as e:
            messagebox.showerror("Erreur", f"Impossible d'écrire le YAML par défaut: {e}")

    # ----- Règles rapides -----
    def _build_simple_regex(self, sample: str, bow: bool) -> str:
        s = sample.strip()
        s = re.sub(r"\s+", r"\\s+", re.escape(s))
        return rf"\b{s}\b" if bow else s

    def _preview_rule(self):
        sample = self.rule_example.get().strip()
        if not sample:
            messagebox.showinfo("Info", "Exemple vide."); return
        rtype = self.rule_type.get(); ic = self.flag_ic.get(); bow = self.flag_bow.get()
        placeholder = self.rule_placeholder.get().strip() or "[MASK]"

        if rtype == "Mot exact":
            pattern = self._build_simple_regex(sample, bow)
        elif rtype == "Forme proche":
            pattern = self._build_simple_regex(sample, bow)
        else:
            pattern = sample  # modèle avancé (regex)

        try:
            rx = re.compile(pattern, re.IGNORECASE if ic else 0)
        except Exception as e:
            messagebox.showerror("Modèle invalide", str(e)); return

        # Prévisualisation sur le premier PDF du dossier
        folder = Path(self.dir_var.get().strip())
        pdfs = sorted([p for p in folder.glob("*.pdf") if p.is_file()]) if folder.is_dir() else []
        if not pdfs:
            messagebox.showinfo("Info", "Aucun PDF pour prévisualiser."); return
        try:
            pages_text, tables_lines = core.extract_text_two_passes(pdfs[0])  # type: ignore[attr-defined]
            text = "\n".join(pages_text) + "\n\n" + "\n".join("\n".join(r) for r in tables_lines)
            hits = len(rx.findall(text))
            self._log(f"Prévisualisation : {hits} occurrence(s) sur {pdfs[0].name}")
        except Exception as e:
            self._log(f"Prévisualisation indisponible: {e}")

    def _save_rule(self):
        if yaml is None:
            messagebox.showerror("PyYAML manquant", "Installez PyYAML (pip install pyyaml).")
            return
        sample = self.rule_example.get().strip()
        if not sample:
            messagebox.showinfo("Info", "Exemple vide."); return
        rtype = self.rule_type.get(); ic = self.flag_ic.get(); bow = self.flag_bow.get()
        placeholder = self.rule_placeholder.get().strip() or "[MASK]"
        scope = self.rule_scope.get()

        cfg = self.cfg_data or {}
        cfg.setdefault("blacklist", {})
        cfg.setdefault("regex_overrides", [])

        if rtype in ("Mot exact", "Forme proche"):
            # On utilise la blacklist simple
            if rtype == "Mot exact":
                lst = cfg["blacklist"].setdefault("force_mask_terms", [])
                if sample not in lst:
                    lst.append(sample)
            else:
                pattern = self._build_simple_regex(sample, bow)
                lst = cfg["blacklist"].setdefault("force_mask_regex", [])
                if pattern not in lst:
                    lst.append(pattern)
        else:
            # Modèle avancé → override avec placeholder explicite
            entry = {
                "name": f"custom_{len(cfg['regex_overrides'])+1}",
                "pattern": sample,
                "placeholder": placeholder,
                "flags": ["IGNORECASE"] if ic else [],
                "scope": scope,
            }
            cfg["regex_overrides"].append(entry)

        self.cfg_data = cfg
        self._save_cfg()
        self._log("Règle ajoutée. Cliquez sur Recharger pour l'appliquer.")

    # ----- Actions -----
    def _browse(self):
        d = filedialog.askdirectory()
        if d:
            self.dir_var.set(d)

    def _run(self):
        folder = Path(self.dir_var.get().strip())
        if not folder.is_dir():
            messagebox.showwarning("Dossier invalide", "Choisissez un dossier contenant des PDF.")
            return
        self.btn_run.config(state=tk.DISABLED)
        threading.Thread(target=self._worker, args=(folder,), daemon=True).start()

    def _worker(self, folder: Path):
        try:
            pdfs = sorted([p for p in folder.glob("*.pdf") if p.is_file()])
            if not pdfs:
                self._log("Aucun PDF trouvé.")
                return
            outdir = folder / "pseudonymise"
            outdir.mkdir(exist_ok=True)
            ok = ko = 0
            global_counts: Dict[str,int] = {}
            for i, pdf in enumerate(pdfs, start=1):
                self.status_var.set(f"{i}/{len(pdfs)} — {pdf.name}")
                make_vec = (self.format_var.get() == "vector")
                make_ras = (self.format_var.get() == "raster")
                try:
                    outputs = core.process_pdf(
                        pdf_path=pdf,
                        out_dir=outdir,
                        make_vector_redaction=make_vec,
                        also_make_raster_burn=make_ras,
                        config_path=Path(self.cfg_path.get()),
                    )
                    self._log("✓ " + pdf.name)
                    for k, v in outputs.items():
                        self._log(f"  - {k}: {v}")
                    # Résumé par doc (compte des remplacements)
                    audit_path = Path(outputs.get("audit", ""))
                    counts = self._count_audit(audit_path)
                    if counts:
                        self._log("  ~ résumé : " + ", ".join(f"{k}={v}" for k, v in sorted(counts.items())))
                        for k,v in counts.items():
                            global_counts[k] = global_counts.get(k,0)+v
                    ok += 1
                except Exception as e:
                    self._log(f"✗ {pdf.name} → ERREUR: {e}")
                    ko += 1
            self.status_var.set(f"Terminé : {ok} OK, {ko} erreurs. Sortie: {outdir}")
            if ok:
                self._log("—")
                self._log("RÉSUMÉ DU LOT : " + ", ".join(f"{k}={v}" for k, v in sorted(global_counts.items())))
                self.btn_open_out.config(state=tk.NORMAL)
                self._last_outdir = outdir
        finally:
            self.btn_run.config(state=tk.NORMAL)

    def _count_audit(self, audit_path: Path) -> Dict[str,int]:
        d: Dict[str,int] = {}
        try:
            with open(audit_path, "r", encoding="utf-8") as f:
                for line in f:
                    try:
                        obj = json.loads(line)
                        k = obj.get("kind", "?")
                        d[k] = d.get(k,0)+1
                    except Exception:
                        pass
        except Exception:
            pass
        return d

    def _open_out(self):
        p = getattr(self, "_last_outdir", None)
        if p:
            open_folder(p)

    def _pump_logs(self):
        try:
            while True:
                msg = self.queue.get_nowait()
                self.txt.insert(tk.END, msg + "\n"); self.txt.see(tk.END)
        except queue.Empty:
            pass
        finally:
            self.root.after(60, self._pump_logs)

    def _log(self, msg: str):
        self.queue.put(msg)

    def _show_help(self):
        messagebox.showinfo(
            "Aide (2 minutes)",
            "1) Choisissez un dossier avec vos PDF.\n"
            "2) Choisissez le format du document final.\n"
            "   - PDF anonymisé (léger) : texte supprimé + boîtes noires (sélection possible).\n"
            "   - PDF image (très sûr) : chaque page en image, aucun texte résiduel.\n"
            "3) Cliquez sur Anonymiser.\n"
            "4) Ouvrez le dossier de résultats pour vérifier.\n"
            "5) Onglet Avancé : ajustez les règles si besoin (mots à garder, à masquer, modèles).",
        )

# ---------- main ----------
if __name__ == "__main__":
    root = tk.Tk()
    App(root)
    root.mainloop()
