"""Onglet Configuration de la GUI V6.

L'onglet est construit une seule fois : les sous-sections sont des panneaux
préchargés puis simplement relevés avec ``tkraise()``. Cela évite le rendu
progressif et les micro-latences visibles lors du passage Réglages/Masquage/
Partage/Règles.
"""

from __future__ import annotations

import sys
from pathlib import Path
from tkinter import filedialog, messagebox

import customtkinter as ctk

import engine_capabilities
from gui_v6 import theme as theme_mod
from gui_v6 import ui_kit
from gui_v6.config_state import ConfigState, default_profile_key, list_profile_keys
from manual_masking import ensure_mask_templates_dir, list_mask_templates, mask_template_label

_SUBTABS = [
    ("reg", "⚙️ Réglages"),
    ("pro", "👤 Profils"),
    ("shr", "🔄 Partage"),
]

_DETECTION_OPTIONS = [
    ("Noms et prénoms", "Annuaire + IA"),
    ("Dates de naissance", "Contexte naissance"),
    ("Établissements", "FINESS + contexte"),
    ("Adresses / CP", "Voie, ville, code"),
    ("N° sécurité sociale", "NIR"),
    ("Téléphones / e-mails", "Contact"),
    ("N° adhérent mutuelle", "Identifiant local"),
]

_MASK_COLORS = [
    ("Noir", "#000000"),
    ("Bleu nuit", "#1a1a2e"),
    ("Gris", "#374151"),
    ("Marron", "#92400e"),
    ("Bleu marine", "#1e3a5f"),
]

MANUAL_MASK_NONE_LABEL = "Aucun masque manuel"

MINI_TOGGLE_HEIGHT = 46
MINI_TOGGLE_LABEL_FONT_SIZE = 12
MINI_TOGGLE_HINT_FONT_SIZE = 11

# Textes d'aide « ? » (français simple, pour utilisateurs non informaticiens).
_HELP_REGLAGES = (
    "Réglages de l'anonymisation.\n\n"
    "• Profil d'anonymisation : choisit un jeu de réglages adapté à votre usage.\n"
    "• Moteurs NER : les modèles qui détectent les noms et données personnelles.\n"
    "• Données à détecter : ce qui sera masqué (noms, dates de naissance, etc.).\n"
    "• Listes locales : vos termes à toujours masquer ou toujours conserver.\n\n"
    "Tout fonctionne 100 % en local sur ce poste. Aucun document patient n'est envoyé sur Internet."
)
_HELP_PARTAGE = (
    "À quoi sert le Partage ?\n\n"
    "Il permet d'échanger les RÉGLAGES de l'application (listes de termes, règles, "
    "style de masquage, modèle de masque) entre plusieurs postes, ou avec votre administrateur.\n\n"
    "• Exporter : enregistre vos réglages dans un fichier .json à transmettre.\n"
    "• Importer : fusionne des réglages reçus avec les vôtres.\n\n"
    "IMPORTANT : seuls les réglages sont partagés. Vos documents patients ne sont JAMAIS "
    "partagés ni envoyés sur Internet."
)
_HELP_REGLES = (
    "Les Règles adaptent le moteur à votre établissement (ex. : toujours masquer un sigle, "
    "toujours conserver un terme métier).\n\n"
    "Chaque règle est validée avant d'être activée.\n\n"
    "Cette section est en cours de finalisation : les actions marquées « à venir » "
    "ne sont pas encore disponibles."
)
_HELP_PROFIL = (
    "Un profil d'anonymisation regroupe tous les réglages adaptés à un usage "
    "(ex. : interne standard, diffusion prudente, recherche…).\n\n"
    "Il définit les moteurs utilisés, les données détectées, les termes à conserver "
    "ou à masquer, et si un masque manuel est requis.\n\n"
    "Choisissez un profil ici ; ses termes sont consultables via « Ouvrir le tableau des termes »."
)
_HELP_MOTEURS = (
    "Les moteurs détectent les données personnelles.\n\n"
    "• CamemBERT-bio : moteur standard, rapide et fiable — activé par défaut.\n"
    "• EDS-Pseudo et GLiNER : optionnels. Ils renforcent la détection mais sont "
    "plus lents et ne sont pas toujours installés sur le poste.\n\n"
    "Si tout n'est pas coché, c'est que les moteurs optionnels ne sont pas requis "
    "par le profil ou pas disponibles."
)
_HELP_LISTES = (
    "Les listes locales personnalisent la détection pour votre établissement :\n\n"
    "• À conserver : termes à ne jamais masquer (vocabulaire métier).\n"
    "• À masquer : termes à toujours masquer (sigles, en-têtes…).\n"
    "• À ignorer : mots à ne pas considérer.\n\n"
    "Pour une liste longue, ouvrez le tableau des termes : "
    "il reste lisible et permet la recherche."
)
_HELP_DONNEES_DETECTER = (
    "Cette zone indique les familles de données que le profil cherche à anonymiser : "
    "noms, dates de naissance, établissements, adresses, identifiants, téléphones et e-mails.\n\n"
    "Ces options décrivent le périmètre fonctionnel attendu. Les règles exactes restent "
    "contrôlées par le moteur et par le profil actif."
)
_HELP_MOTEURS_MASQUES = (
    "Cette zone regroupe les moteurs de détection et les réglages de masque manuel.\n\n"
    "Le masque manuel sert aux zones fixes d'un document que le texte ne suffit pas à détecter "
    "correctement : logo, en-tête, coordonnées, bloc institutionnel ou tampon scanné.\n\n"
    "Si « Masque manuel obligatoire » est actif, le profil impose cette étape de contrôle "
    "avant de considérer le traitement complet."
)
_HELP_PROFIL_CHOIX = (
    "Choisissez ici le profil à modifier.\n\n"
    "Les profils livrés par défaut sont en lecture seule pour éviter une modification accidentelle. "
    "Dupliquez un profil pour créer une version adaptée à votre établissement."
)
_HELP_PROFIL_IDENTITE = (
    "Nom et description visibles dans l'interface.\n\n"
    "Utilisez un nom simple que les utilisateurs comprendront, par exemple « Standard local », "
    "« Recherche » ou « Diffusion externe prudente »."
)
_HELP_PROFIL_MASQUAGE = (
    "Cette zone règle les masques propres au profil.\n\n"
    "Masquage manuel obligatoire : le profil impose une vérification avec un masque de zones fixes "
    "avant le traitement. C'est utile pour les documents qui ont toujours les mêmes zones sensibles "
    "au même endroit : logos, en-têtes, coordonnées, tampons ou blocs scannés.\n\n"
    "Template de masque préféré : modèle proposé automatiquement par ce profil. "
    "L'éditeur de masque permet de créer ou ajuster ces zones visuellement."
)
_HELP_PROFIL_MOTEURS = (
    "Cette zone précise les moteurs utilisés par le profil.\n\n"
    "CamemBERT-bio est le moteur standard. Les moteurs optionnels ne sont proposés que s'ils sont "
    "réellement embarqués dans cette version. Le moteur VLM concerne surtout les documents images."
)
_HELP_PROFIL_MOTS = (
    "Ces listes appartiennent au profil.\n\n"
    "À masquer : termes à remplacer systématiquement.\n"
    "À conserver : termes à ne jamais masquer, même s'ils ressemblent à des noms.\n"
    "À ignorer : mots qui ne doivent pas déclencher de détection.\n\n"
    "Pour de longues listes, utilisez le tableau des termes afin de rechercher et vérifier plus facilement."
)
_HELP_EXPORT_CONFIG = (
    "Exporte uniquement les réglages de l'application : profils, listes locales, règles et style de masque.\n\n"
    "Les documents patients, résultats d'anonymisation et audits ne sont pas exportés."
)
_HELP_IMPORT_CONFIG = (
    "Importe des réglages reçus d'un administrateur ou d'un autre poste.\n\n"
    "L'import ne lit pas de documents patients. Vérifiez toujours le profil actif après import."
)

CONFIG_MOCKUP_SECTIONS = {
    "reglages": [
        "Profil d'anonymisation",
        "Moteurs NER",
        "Données à détecter",
        "Termes à toujours conserver",
        "Termes à toujours masquer",
        "Masque manuel obligatoire",
        "Template de masque manuel",
    ],
    "masquage": [
        "Couleur de masquage (PDF)",
        "Style des marqueurs (texte)",
        "Épaisseur du masque",
        "Codes de remplacement",
        "Masques de zones fixes",
        "Éditeur interactif de masques",
    ],
    "partage": ["Exporter la configuration", "Importer une configuration"],
    "regles": ["Règles actives", "Testeur de règle"],
}

CONFIG_INTERACTION_CONTRACT = {
    "subtabs": "prebuilt_panels",
    "reglages_columns": 3,
    "mask_editor": [
        "open_pdf",
        "draw_rectangle",
        "delete_rectangle_on_click",
        "zoom",
        "save_template_json",
        "load_template_json_or_yaml",
        "clear_page",
        "apply_template_selection",
    ],
}


def _app_base_dir() -> Path:
    if getattr(sys, "frozen", False):
        return Path(sys.executable).resolve().parent
    return Path.cwd()


class ConfigTab(ctk.CTkFrame):
    def __init__(self, master, state: ConfigState | None = None, palette: dict | None = None, **kwargs):
        self._p = palette or theme_mod.get_palette(theme_mod.DEFAULT_THEME)
        super().__init__(master, fg_color=self._p["bg"], **kwargs)
        self._state = state if state is not None else ConfigState()
        self._sub = "reg"
        self._sub_buttons: dict[str, ctk.CTkButton] = {}
        self._panels: dict[str, ctk.CTkFrame] = {}

        self._manual_mask_templates: dict[str, Path | None] = {MANUAL_MASK_NONE_LABEL: None}
        self._manual_mask_var = ctk.StringVar(value=MANUAL_MASK_NONE_LABEL)
        self._manual_mask_required_var = ctk.BooleanVar(value=self._state.manual_mask_required)

        self._mask_style_var = ctk.StringVar(value=self._state.mask_marker_style)
        self._mask_color = self._state.mask_color
        self._mask_margin_x_var = ctk.IntVar(value=self._state.mask_margin_x)
        self._mask_margin_y_var = ctk.IntVar(value=self._state.mask_margin_y)
        self._mask_rounded_var = ctk.BooleanVar(value=self._state.mask_rounded_corners)
        self._mask_status_text = ctk.StringVar(value="Éditeur de masques en fenêtre dédiée.")

        # L'édition interactive des masques se fait dans une fenêtre séparée
        # (gui_v6.mask_editor_window) ; on garde juste une référence à l'instance
        # ouverte pour éviter d'en empiler plusieurs.
        self._mask_editor_window = None

        # Éditeur de profils : chemin overlay runtime (None = config/profiles.yml standard,
        # surchargeable en test), clé en cours d'édition + widgets.
        self._profiles_path = None
        self._pro_edit_key: str | None = None
        self._pro_term_lists: dict = {}
        self._profile_scroll = None

        self._build()

    @property
    def state(self) -> ConfigState:
        return self._state

    # -- construction -----------------------------------------------------

    def _build(self) -> None:
        p = self._p
        bar = ctk.CTkFrame(self, fg_color="transparent")
        bar.pack(fill="x", padx=14, pady=(10, 2))
        for key, label in _SUBTABS:
            btn = ctk.CTkButton(
                bar,
                text=label,
                command=lambda k=key: self._show_sub(k),
                fg_color=p["btn_sec_bg"] if key == self._sub else "transparent",
                hover_color=p["card_border"],
                text_color=p["primary"] if key == self._sub else p["text_dim"],
                border_color=p["card_border"],
                border_width=1 if key == self._sub else 0,
                font=ui_kit.font(13, "bold" if key == self._sub else "normal"),
                corner_radius=6,
                width=10,
                height=30,
            )
            btn.pack(side="left", padx=(0, 6))
            self._sub_buttons[key] = btn

        self._body = ctk.CTkFrame(self, fg_color="transparent")
        self._body.pack(fill="both", expand=True, padx=14, pady=(4, 0))
        self._body.grid_columnconfigure(0, weight=1)
        self._body.grid_rowconfigure(0, weight=1)

        builders = {
            "reg": self._build_reglages,
            "pro": self._build_profils,
            "shr": self._build_partage,
        }
        for key, builder in builders.items():
            panel = ctk.CTkFrame(self._body, fg_color="transparent")
            panel.grid(row=0, column=0, sticky="nsew")
            self._panels[key] = panel
            builder(panel)

        self._refresh_manual_mask_templates()
        self._show_sub("reg")

    def _show_sub(self, key: str) -> None:
        self._sub = key
        p = self._p
        for k, btn in self._sub_buttons.items():
            active = k == key
            btn.configure(
                fg_color=p["btn_sec_bg"] if active else "transparent",
                border_width=1 if active else 0,
                text_color=p["primary"] if active else p["text_dim"],
                font=ui_kit.font(13, "bold" if active else "normal"),
            )
        self._panels[key].tkraise()

    # -- Réglages ---------------------------------------------------------

    def _build_reglages(self, parent) -> None:
        p = self._p
        self._section_intro(
            parent,
            "Choisissez ce que l'application doit détecter et masquer. Tout reste local.",
            _HELP_REGLAGES,
            "Les Réglages",
        )
        top = ctk.CTkFrame(
            parent,
            fg_color=p["card"],
            border_color=p["card_border"],
            border_width=1,
            corner_radius=8,
        )
        top.pack(fill="x", pady=(0, 8))

        profiles = list_profile_keys()
        current = self._state.profile or default_profile_key() or (profiles[0] if profiles else "")
        self._state.profile = current or None

        ctk.CTkLabel(top, text="Profil d'anonymisation", text_color=p["text_dim"], font=ui_kit.font(11, "bold")).pack(
            side="left", padx=(12, 8), pady=10
        )
        self._profile_menu = ctk.CTkOptionMenu(
            top,
            values=profiles or ["(aucun profil)"],
            command=self._on_profile,
            fg_color=p["btn_sec_bg"],
            button_color=p["primary"],
            button_hover_color=p["primary_dim"],
            text_color=p["text"],
            width=180,
            height=30,
        )
        if current:
            self._profile_menu.set(current)
        self._profile_menu.pack(side="left", pady=10)
        ui_kit.help_button(top, p, _HELP_PROFIL, title="Profil d'anonymisation").pack(side="left", padx=(6, 0), pady=10)
        ui_kit.secondary_button(top, p, "✏️ Modifier le profil…", command=self._open_profile_editor).pack(
            side="left", padx=(10, 4), pady=10
        )

        sortie = ui_kit.secondary_button(top, p, "📁 Dossier de sortie…", command=self._pick_output)
        sortie.pack(side="left", padx=(6, 6), pady=10)
        ui_kit.attach_tooltip(
            sortie, "Dossier où seront écrits les documents anonymisés.\nRéglage local de traitement (pas une règle du profil)."
        )
        self._out_label = ctk.CTkLabel(
            top,
            text=str(self._state.output_dir or "anonymise/"),
            text_color=p["text_muted"],
            font=ui_kit.font(12),
        )
        self._out_label.pack(side="left", fill="x", expand=True, padx=(0, 12), pady=10)

        cols = self._columns(parent, 3, gap=8, height=455)

        det = ui_kit.Card(
            cols[0], p, title="🔍 Données à détecter",
            help_text=_HELP_DONNEES_DETECTER, help_title="Données à détecter",
        )
        det.pack(fill="both", expand=True)
        for label, hint in _DETECTION_OPTIONS:
            self._mini_toggle(det, label, hint, value=True).pack(fill="x", padx=12, pady=1)

        ner = ui_kit.Card(
            cols[1], p, title="🧠 Moteurs et masques",
            help_text=_HELP_MOTEURS_MASQUES, help_title="Moteurs et masques",
        )
        ner.pack(fill="both", expand=True)
        hint_row = ctk.CTkFrame(ner, fg_color="transparent")
        hint_row.pack(fill="x", padx=12, pady=(0, 2))
        ctk.CTkLabel(
            hint_row, text="Pourquoi pas tout coché ?", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w"
        ).pack(side="left")
        ui_kit.help_button(hint_row, p, _HELP_MOTEURS, title="Moteurs de détection").pack(side="right")
        # Honnêteté moteurs : ne pas proposer un moteur que ce build n'embarque pas.
        caps = engine_capabilities.capabilities_map()
        eds_off = not caps["eds"].available
        gli_off = not caps["gliner"].available
        if eds_off:
            self._state.enable_eds = False
        if gli_off:
            self._state.enable_gliner = False
        self._tog_ner = self._mini_toggle(
            ner, "CamemBERT-bio", "standard · rapide · F1 0.963", value=self._state.use_local_ner, command=self._on_ner
        )
        self._tog_ner.pack(fill="x", padx=12, pady=1)
        self._tog_eds = self._mini_toggle(
            ner, "EDS-Pseudo", "optionnel · médical français · plus lent", value=self._state.enable_eds,
            command=self._on_eds, disabled=eds_off, disabled_hint="non embarqué dans cette version",
        )
        self._tog_eds.pack(fill="x", padx=12, pady=1)
        self._tog_gli = self._mini_toggle(
            ner, "GLiNER", "optionnel · vote croisé · plus lent", value=self._state.enable_gliner,
            command=self._on_gliner, disabled=gli_off, disabled_hint="non embarqué dans cette version",
        )
        self._tog_gli.pack(fill="x", padx=12, pady=1)
        self._mini_toggle(
            ner,
            "Masque manuel obligatoire",
            "bloque le traitement si absent",
            value=self._state.manual_mask_required,
            variable=self._manual_mask_required_var,
            command=self._on_manual_mask_required,
        ).pack(fill="x", padx=12, pady=(8, 1))

        ctk.CTkLabel(
            ner,
            text="Template de masque manuel",
            text_color=p["text_muted"],
            font=ui_kit.font(11, "bold"),
            anchor="w",
        ).pack(fill="x", padx=12, pady=(10, 2))
        self._manual_mask_menu = ctk.CTkOptionMenu(
            ner,
            values=[MANUAL_MASK_NONE_LABEL],
            variable=self._manual_mask_var,
            command=self._on_manual_mask_template,
            fg_color=p["btn_sec_bg"],
            button_color=p["primary"],
            button_hover_color=p["primary_dim"],
            text_color=p["text"],
            height=30,
        )
        self._manual_mask_menu.pack(fill="x", padx=12, pady=(0, 6))
        mask_actions = ctk.CTkFrame(ner, fg_color="transparent")
        mask_actions.pack(fill="x", padx=12, pady=(0, 12))
        ui_kit.secondary_button(mask_actions, p, "🔄 Actualiser", command=self._refresh_manual_mask_templates).pack(
            side="left", fill="x", expand=True
        )

        terms = ui_kit.Card(
            cols[2], p, title="✅ Listes locales",
            help_text=_HELP_LISTES, help_title="Listes locales",
        )
        terms.pack(fill="both", expand=True)
        terms_help = ctk.CTkFrame(terms, fg_color="transparent")
        terms_help.pack(fill="x", padx=12, pady=(0, 2))
        ctk.CTkLabel(
            terms_help, text="Termes propres à votre établissement", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w"
        ).pack(side="left")
        ui_kit.help_button(terms_help, p, _HELP_LISTES, title="Listes locales").pack(side="right")
        ctk.CTkLabel(
            terms,
            text="Les termes du profil actif sont consultables dans un tableau dédié.",
            text_color=p["text_dim"],
            font=ui_kit.font(12),
            justify="left",
            wraplength=240,
            anchor="w",
        ).pack(fill="x", padx=12, pady=(2, 6))
        counts = self._active_profile_summary().list_counts
        chips = ctk.CTkFrame(terms, fg_color="transparent")
        chips.pack(fill="x", padx=12, pady=(0, 8))
        for label, count in counts.items():
            ctk.CTkLabel(
                chips,
                text=f"{label} : {count}",
                text_color=p["text"],
                fg_color=p["divider"],
                corner_radius=8,
                font=ui_kit.font(11, "bold"),
            ).pack(side="left", padx=(0, 6), ipadx=7, ipady=2)
        ui_kit.primary_button(
            terms, p, "📋 Ouvrir le tableau des termes", command=self._open_terms_table
        ).pack(fill="x", padx=12, pady=(2, 12))

    # -- Profil actif / tableau des termes --------------------------------

    def _active_profile_dict(self) -> dict:
        try:
            from profile_defaults import list_effective_profiles

            key = self._state.profile or default_profile_key()
            if not key:
                return {}
            return list_effective_profiles().get(key, {}) or {}
        except Exception:
            return {}

    def _active_profile_summary(self):
        from gui_v6.profile_view import summarize_profile

        key = self._state.profile or default_profile_key() or ""
        return summarize_profile(key, self._active_profile_dict())

    def _open_terms_table(self) -> None:
        from gui_v6.profile_view import profile_term_rows
        from gui_v6.terms_table_window import TermsTableWindow

        summary = self._active_profile_summary()
        rows = profile_term_rows(self._active_profile_dict())
        TermsTableWindow(self.winfo_toplevel(), self._p, rows, profile_label=summary.label)

    # -- Profils (éditeur) ------------------------------------------------

    def _build_profils(self, parent) -> None:
        p = self._p
        from gui_v6.editable_list import EditableTermList

        # L'application fournit déjà un scroll vertical global. Un second
        # CTkScrollableFrame imbriqué coupe la page Profils sous Windows et
        # laisse un grand bloc vide en bas de fenêtre.
        self._profile_scroll = None

        self._section_intro(
            parent,
            "Un profil regroupe les moteurs, les masques, les règles et les mots à conserver ou masquer.",
            _HELP_PROFIL,
            "Profils d'anonymisation",
        )

        bar = ui_kit.Card(
            parent, p, title="👤 Profil à modifier",
            help_text=_HELP_PROFIL_CHOIX, help_title="Profil à modifier",
        )
        bar.pack(fill="x", pady=(0, 8))
        top = ctk.CTkFrame(bar, fg_color="transparent")
        top.pack(fill="x", padx=12, pady=(0, 4))
        self._pro_menu_var = ctk.StringVar(value="")
        self._pro_menu = ctk.CTkOptionMenu(
            top, values=["—"], variable=self._pro_menu_var, command=self._pro_on_select,
            fg_color=p["btn_sec_bg"], button_color=p["primary"], button_hover_color=p["primary_dim"],
            text_color=p["text"], width=260, height=30,
        )
        self._pro_menu.pack(side="left")
        self._pro_status = ctk.CTkLabel(top, text="", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w")
        self._pro_status.pack(side="left", padx=(10, 0))

        actions = ctk.CTkFrame(bar, fg_color="transparent")
        actions.pack(fill="x", padx=12, pady=(0, 12))
        ui_kit.secondary_button(actions, p, "+ Nouveau", command=self._pro_new).pack(side="left", padx=(0, 6))
        ui_kit.secondary_button(actions, p, "⧉ Dupliquer", command=self._pro_duplicate).pack(side="left", padx=(0, 6))
        self._pro_save_btn = ui_kit.primary_button(actions, p, "💾 Enregistrer", command=self._pro_save)
        self._pro_save_btn.pack(side="left", padx=(6, 6))
        ui_kit.secondary_button(actions, p, "↩ Annuler", command=self._pro_cancel).pack(side="left")
        self._pro_default_btn = ui_kit.secondary_button(actions, p, "⭐ Définir par défaut", command=self._pro_set_default)
        self._pro_default_btn.pack(side="right")

        cols = self._columns(parent, 2, gap=8)
        left, right = cols[0], cols[1]

        ident = ui_kit.Card(
            left, p, title="🏷️ Identité",
            help_text=_HELP_PROFIL_IDENTITE, help_title="Identité du profil",
        )
        ident.pack(fill="x", pady=(0, 8))
        self._pro_label_var = ctk.StringVar()
        self._pro_label_entry = ctk.CTkEntry(ident, textvariable=self._pro_label_var, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=30)
        ctk.CTkLabel(ident, text="Nom du profil", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").pack(fill="x", padx=12, pady=(0, 2))
        self._pro_label_entry.pack(fill="x", padx=12, pady=(0, 6))
        self._pro_desc_var = ctk.StringVar()
        self._pro_desc_entry = ctk.CTkEntry(ident, textvariable=self._pro_desc_var, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=30)
        ctk.CTkLabel(ident, text="Description", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").pack(fill="x", padx=12, pady=(0, 2))
        self._pro_desc_entry.pack(fill="x", padx=12, pady=(0, 12))

        eng = ui_kit.Card(
            right, p, title="🧠 Moteurs",
            help_text=_HELP_PROFIL_MOTEURS, help_title="Moteurs du profil",
        )
        eng.pack(fill="x", pady=(0, 8))
        self._pro_disable_vlm_var = ctk.BooleanVar(value=False)
        self._pro_vlm_switch = ctk.CTkSwitch(eng, text="Désactiver le moteur VLM (images)", variable=self._pro_disable_vlm_var, progress_color=p["primary"], text_color=p["text"], font=ui_kit.font(12))
        self._pro_vlm_switch.pack(anchor="w", padx=12, pady=(0, 6))
        # Note honnête : reflète les moteurs réellement embarqués par ce build.
        caps_pro = engine_capabilities.capabilities_map()
        opt = [c.label.split(" (")[0] for c in (caps_pro["eds"], caps_pro["gliner"]) if c.available]
        if opt:
            moteurs_note = "CamemBERT-bio (standard) toujours actif ; " + " / ".join(opt) + " disponibles (optionnels)."
        else:
            moteurs_note = "CamemBERT-bio (standard) toujours actif ; EDS-Pseudo / GLiNER non embarqués dans cette version."
        ctk.CTkLabel(eng, text=moteurs_note, text_color=p["text_muted"], font=ui_kit.font(11), anchor="w", wraplength=300, justify="left").pack(fill="x", padx=12, pady=(0, 12))

        mask = ui_kit.Card(
            right, p, title="⬛ Masquage",
            help_text=_HELP_PROFIL_MASQUAGE, help_title="Masquage manuel",
        )
        mask.pack(fill="x", pady=(0, 8))
        self._pro_require_mask_var = ctk.BooleanVar(value=False)
        self._pro_require_switch = ctk.CTkSwitch(mask, text="Masque manuel obligatoire", variable=self._pro_require_mask_var, progress_color=p["primary"], text_color=p["text"], font=ui_kit.font(12))
        self._pro_require_switch.pack(anchor="w", padx=12, pady=(0, 6))
        ctk.CTkLabel(mask, text="Template de masque préféré", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").pack(fill="x", padx=12, pady=(0, 2))
        self._pro_template_var = ctk.StringVar()
        self._pro_template_entry = ctk.CTkEntry(mask, textvariable=self._pro_template_var, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=30)
        self._pro_template_entry.pack(fill="x", padx=12, pady=(0, 6))
        mask_actions = ctk.CTkFrame(mask, fg_color="transparent")
        mask_actions.pack(fill="x", padx=12, pady=(0, 6))
        ui_kit.secondary_button(mask_actions, p, "🖊 Ouvrir l'éditeur de masque", command=self._open_full_mask_editor).pack(side="left")

        # Apparence du masque (couleur / style / marges) — réglage global appliqué aux PDF.
        ctk.CTkLabel(mask, text="Apparence du masque", text_color=p["text_muted"], font=ui_kit.font(11, "bold"), anchor="w").pack(fill="x", padx=12, pady=(6, 2))
        swatches = ctk.CTkFrame(mask, fg_color="transparent")
        swatches.pack(fill="x", padx=12, pady=(0, 6))
        self._swatch_buttons: dict[str, ctk.CTkButton] = {}
        for _label, color_value in _MASK_COLORS:
            btn = ctk.CTkButton(
                swatches, text="", width=28, height=24, fg_color=color_value, hover_color=color_value,
                border_color=p["primary"] if color_value == self._mask_color else p["card_border"],
                border_width=3 if color_value == self._mask_color else 1, corner_radius=6,
                command=lambda c=color_value: self._set_mask_color(c),
            )
            btn.pack(side="left", padx=(0, 6))
            self._swatch_buttons[color_value] = btn
        style_row = ctk.CTkFrame(mask, fg_color="transparent")
        style_row.pack(fill="x", padx=12, pady=(0, 6))
        for _label, value, preview in [("Crochets", "brackets", "[NOM]"), ("Étoiles", "stars", "***"), ("Noirci", "blackout", "████")]:
            ctk.CTkRadioButton(
                style_row, text=preview, variable=self._mask_style_var, value=value, command=self._update_mask_preview,
                text_color=p["text"], fg_color=p["primary"], hover_color=p["primary_dim"], font=ui_kit.font(11),
            ).pack(side="left", padx=(0, 10))
        self._slider_row(mask, "Marge H", self._mask_margin_x_var, self._on_mask_margin_x)
        self._slider_row(mask, "Marge V", self._mask_margin_y_var, self._on_mask_margin_y)
        self._mini_toggle(
            mask, "Coins arrondis", "", value=self._state.mask_rounded_corners,
            variable=self._mask_rounded_var, command=self._on_rounded_corners,
        ).pack(fill="x", padx=12, pady=(2, 12))

        words = ui_kit.Card(
            parent, p, title="📝 Mots du profil",
            help_text=_HELP_PROFIL_MOTS, help_title="Mots du profil",
        )
        words.pack(fill="x", pady=(8, 0))
        self._pro_term_lists = {
            "blacklist": EditableTermList(words, p, title="À masquer", height=78),
            "whitelist": EditableTermList(words, p, title="À conserver", height=78),
            "stopwords": EditableTermList(words, p, title="À ignorer", height=66),
        }
        for tl in self._pro_term_lists.values():
            tl.pack(fill="x", padx=12, pady=(0, 8))

        rules = ui_kit.Card(
            parent, p, title="🛡️ Règles du profil",
            help_text=_HELP_REGLES, help_title="Règles du profil",
        )
        rules.pack(fill="x", pady=(8, 0))
        rules_intro = ctk.CTkFrame(rules, fg_color="transparent")
        rules_intro.pack(fill="x", padx=12, pady=(0, 2))
        ctk.CTkLabel(
            rules_intro,
            text="Règles d'anonymisation portées par ce profil (adaptées à votre établissement).",
            text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left", wraplength=520,
        ).pack(side="left", padx=(0, 6))
        ui_kit.help_button(rules_intro, p, _HELP_REGLES, title="Les Règles du profil").pack(side="right")
        headers = ctk.CTkFrame(rules, fg_color="transparent")
        headers.pack(fill="x", padx=12, pady=(2, 4))
        for text, width in [("Label", 190), ("Type", 80), ("Cible → Résultat", 210), ("Statut", 70), ("", 70)]:
            ctk.CTkLabel(headers, text=text.upper(), width=width, anchor="w", text_color=p["text_muted"], font=ui_kit.font(10, "bold")).pack(side="left")
        for row in [
            ("Masquer le sigle CHUXX", "exact", "CHUXX → [MASK]", "Actif"),
            ("Préserver “classification internationale”", "preserve", "conservé tel quel", "Actif"),
            ("Identifier N° 1234567", "norm-id", "N° 1234567 → [NDA]", "Candidat"),
        ]:
            self._rule_row(rules, row)
        self._note(rules, "Aperçu illustratif. L'édition fine des règles du profil arrivera dans une prochaine version.")
        self._mockup_button(rules, "+ Ajouter une règle").pack(anchor="w", padx=12, pady=(0, 12))

        self._pro_refresh_and_load()

    # -- Profils : logique -----------------------------------------------

    def _pro_choices(self) -> list:
        from gui_v6.profile_editor import list_profile_choices

        return list_profile_choices(self._profiles_path)

    @staticmethod
    def _pro_label_for(choice: dict) -> str:
        if choice["is_default"]:
            return f"{choice['label']} (défaut)"
        if not choice["editable"]:
            return f"{choice['label']} (lecture seule)"
        return choice["label"]

    def _pro_refresh_and_load(self, select_key: str | None = None) -> None:
        choices = self._pro_choices()
        self._pro_choice_by_label = {self._pro_label_for(c): c for c in choices}
        labels = list(self._pro_choice_by_label) or ["—"]
        self._pro_menu.configure(values=labels)
        target = None
        if select_key is not None:
            target = next((lbl for lbl, c in self._pro_choice_by_label.items() if c["key"] == select_key), None)
        if target is None:
            target = self._pro_menu_var.get() if self._pro_menu_var.get() in self._pro_choice_by_label else labels[0]
        self._pro_menu_var.set(target)
        self._pro_on_select(target)

    def _pro_on_select(self, label: str) -> None:
        choice = getattr(self, "_pro_choice_by_label", {}).get(label)
        if choice is None:
            return
        self._pro_load(choice["key"])

    def _profile_dict_for(self, key: str) -> dict:
        from profile_defaults import list_effective_profiles

        try:
            return list_effective_profiles(self._profiles_path).get(key, {}) or {}
        except Exception:
            return {}

    def _pro_load(self, key: str) -> None:
        from gui_v6.profile_editor import profile_is_editable

        profile = self._profile_dict_for(key)
        self._pro_edit_key = key
        self._pro_label_var.set(str(profile.get("label") or key))
        self._pro_desc_var.set(str(profile.get("description") or ""))
        self._pro_require_mask_var.set(bool(profile.get("require_manual_mask")))
        self._pro_disable_vlm_var.set(bool(profile.get("force_disable_vlm")))
        self._pro_template_var.set(str(profile.get("preferred_manual_mask_template") or ""))
        param_lists = profile.get("param_lists") or {}
        self._pro_term_lists["blacklist"].set_terms(param_lists.get("blacklist_force_mask_terms") or [])
        self._pro_term_lists["whitelist"].set_terms(param_lists.get("whitelist_phrases") or [])
        self._pro_term_lists["stopwords"].set_terms(param_lists.get("additional_stopwords") or [])
        editable = profile_is_editable(key, self._profiles_path)
        self._pro_set_editable(editable)
        self._pro_status.configure(
            text="Éditable" if editable else "Profil par défaut — lecture seule (dupliquez pour modifier)"
        )

    def _pro_set_editable(self, editable: bool) -> None:
        state = "normal" if editable else "disabled"
        for widget in (self._pro_label_entry, self._pro_desc_entry, self._pro_template_entry,
                       self._pro_require_switch, self._pro_vlm_switch, self._pro_save_btn):
            widget.configure(state=state)
        for term_list in self._pro_term_lists.values():
            term_list.set_editable(editable)

    def _pro_collect_spec(self) -> dict:
        from gui_v6.profile_editor import build_profile_spec

        return build_profile_spec(
            label=self._pro_label_var.get(),
            description=self._pro_desc_var.get(),
            require_manual_mask=bool(self._pro_require_mask_var.get()),
            force_disable_vlm=bool(self._pro_disable_vlm_var.get()),
            preferred_manual_mask_template=self._pro_template_var.get(),
            whitelist=self._pro_term_lists["whitelist"].terms(),
            blacklist=self._pro_term_lists["blacklist"].terms(),
            stopwords=self._pro_term_lists["stopwords"].terms(),
        )

    def _pro_unique_key(self, base: str) -> str:
        from gui_v6.profile_editor import slug_for_copy

        existing = {c["key"] for c in self._pro_choices()}
        if base not in existing:
            return base
        return slug_for_copy(base, existing)

    def _pro_new(self) -> None:
        from gui_v6.profile_editor import build_profile_spec, save_profile

        key = self._pro_unique_key("nouveau_profil")
        spec = build_profile_spec(label="Nouveau profil")
        try:
            save_profile(key, spec, path=self._profiles_path)
        except Exception as exc:  # pragma: no cover
            messagebox.showerror("Profils", f"Impossible de créer le profil : {exc}")
            return
        self._pro_refresh_and_load(select_key=key)

    def _pro_duplicate(self) -> None:
        from gui_v6.profile_editor import save_profile, slug_for_copy

        if not self._pro_edit_key:
            return
        existing = {c["key"] for c in self._pro_choices()}
        new_key = slug_for_copy(self._pro_edit_key, existing)
        spec = self._pro_collect_spec()
        spec["label"] = f"{spec['label']} (copie)"
        try:
            save_profile(new_key, spec, path=self._profiles_path)
        except Exception as exc:  # pragma: no cover
            messagebox.showerror("Profils", f"Impossible de dupliquer : {exc}")
            return
        self._pro_refresh_and_load(select_key=new_key)

    def _pro_save(self) -> None:
        from gui_v6.profile_editor import save_profile

        key = self._pro_edit_key
        if not key:
            return
        spec = self._pro_collect_spec()
        try:
            save_profile(key, spec, path=self._profiles_path)
        except Exception as exc:  # pragma: no cover
            messagebox.showerror("Profils", f"Impossible d'enregistrer le profil : {exc}")
            return
        self._pro_refresh_and_load(select_key=key)
        # Confirmation non bloquante (pas de modale qui fige l'app).
        self._pro_status.configure(text=f"✓ Profil « {spec['label']} » enregistré.")

    def _pro_cancel(self) -> None:
        if self._pro_edit_key:
            self._pro_load(self._pro_edit_key)

    def _pro_set_default(self) -> None:
        from gui_v6.profile_editor import set_default_profile

        if not self._pro_edit_key:
            return
        try:
            set_default_profile(self._pro_edit_key, path=self._profiles_path)
        except Exception as exc:  # pragma: no cover
            messagebox.showerror("Profils", f"Impossible de définir par défaut : {exc}")
            return
        self._pro_refresh_and_load(select_key=self._pro_edit_key)

    def _open_profile_editor(self) -> None:
        """Ouvre le sous-onglet Profils sur le profil actif (depuis Réglages)."""
        self._show_sub("pro")
        active = self._state.profile
        if active:
            self._pro_refresh_and_load(select_key=active)

    # -- Masquage ---------------------------------------------------------


    # -- Partage / Règles -------------------------------------------------

    def _build_partage(self, parent) -> None:
        p = self._p
        self._section_intro(
            parent,
            "Partagez vos réglages (jamais vos documents) entre postes ou avec l'administrateur.",
            _HELP_PARTAGE,
            "À quoi sert le Partage ?",
        )
        cols = self._columns(parent, 2, gap=8, height=180)
        export = ui_kit.Card(
            cols[0], p, title="📤 Exporter la configuration",
            help_text=_HELP_EXPORT_CONFIG, help_title="Exporter la configuration",
        )
        export.pack(fill="both", expand=True)
        self._note(export, "Listes locales, règles admin, style de masquage et template actif.")
        self._mockup_button(export, "⬇ Exporter (.json)").pack(anchor="w", padx=12, pady=(0, 12))

        import_card = ui_kit.Card(
            cols[1], p, title="📥 Importer une configuration",
            help_text=_HELP_IMPORT_CONFIG, help_title="Importer une configuration",
        )
        import_card.pack(fill="both", expand=True)
        self._note(import_card, "Fusionne la configuration reçue avec vos réglages locaux.")
        self._mockup_button(import_card, "⬆ Importer (.json)").pack(anchor="w", padx=12, pady=(0, 12))

    # -- helpers aide / maquette -----------------------------------------

    def _section_intro(self, parent, sentence: str, help_text: str, help_title: str) -> None:
        """Ligne d'introduction d'une sous-section : phrase courte + bouton d'aide « ? »."""
        p = self._p
        intro = ctk.CTkFrame(parent, fg_color="transparent")
        intro.pack(fill="x", pady=(0, 6))
        ctk.CTkLabel(
            intro, text=sentence, text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left"
        ).pack(side="left", padx=(2, 6))
        ui_kit.help_button(intro, p, help_text, title=help_title).pack(side="right", padx=2)

    def _mockup_button(self, parent, text: str, primary: bool = False):
        """Bouton de maquette non câblé : désactivé + suffixe « (à venir) » pour
        ne pas laisser croire qu'il fonctionne."""
        p = self._p
        factory = ui_kit.primary_button if primary else ui_kit.secondary_button
        btn = factory(parent, p, f"{text} (à venir)", command=lambda: None)
        btn.configure(state="disabled")
        return btn

    # -- callbacks réglages ----------------------------------------------

    def _on_profile(self, value: str) -> None:
        self._state.profile = value

    def _on_ner(self) -> None:
        self._state.use_local_ner = self._tog_ner.get()

    def _on_eds(self) -> None:
        self._state.enable_eds = self._tog_eds.get()

    def _on_gliner(self) -> None:
        self._state.enable_gliner = self._tog_gli.get()

    def _on_manual_mask_required(self) -> None:
        self._state.manual_mask_required = bool(self._manual_mask_required_var.get())

    def _on_manual_mask_template(self, label: str) -> None:
        self._state.manual_mask_template = self._manual_mask_templates.get(label)

    def _pick_output(self) -> None:
        path = filedialog.askdirectory(title="Dossier de sortie")
        if path:
            self._state.output_dir = Path(path)
            self._out_label.configure(text=str(self._state.output_dir))

    def _refresh_manual_mask_templates(self) -> None:
        selected = self._state.manual_mask_template
        options: dict[str, Path | None] = {MANUAL_MASK_NONE_LABEL: None}
        try:
            for path in list_mask_templates(_app_base_dir()):
                options[mask_template_label(path, _app_base_dir())] = path
        except Exception:
            pass
        self._manual_mask_templates = options
        labels = list(options)
        if hasattr(self, "_manual_mask_menu"):
            self._manual_mask_menu.configure(values=labels)
        selected_label = MANUAL_MASK_NONE_LABEL
        if selected is not None:
            for label, path in options.items():
                if path == selected:
                    selected_label = label
                    break
        self._manual_mask_var.set(selected_label)
        self._state.manual_mask_template = options.get(selected_label)

    def _open_templates_dir(self) -> None:
        path = ensure_mask_templates_dir(_app_base_dir())
        messagebox.showinfo("Dossier modèles", str(path))

    # -- callbacks masquage ----------------------------------------------

    def _set_mask_color(self, color: str) -> None:
        self._mask_color = color
        self._state.mask_color = color
        p = self._p
        for value, btn in self._swatch_buttons.items():
            btn.configure(
                border_color=p["primary"] if value == color else p["card_border"],
                border_width=3 if value == color else 1,
            )

    def _on_mask_margin_x(self, value: float) -> None:
        self._state.mask_margin_x = int(round(value))
        self._mask_margin_x_var.set(self._state.mask_margin_x)

    def _on_mask_margin_y(self, value: float) -> None:
        self._state.mask_margin_y = int(round(value))
        self._mask_margin_y_var.set(self._state.mask_margin_y)

    def _on_rounded_corners(self) -> None:
        self._state.mask_rounded_corners = bool(self._mask_rounded_var.get())

    def _update_mask_preview(self) -> None:
        value = self._mask_style_var.get()
        self._state.mask_marker_style = value
        if value == "stars":
            text = "Patient ***, né le ***"
        elif value == "blackout":
            text = "Patient ████, né le ████"
        else:
            text = "Patient [NOM], né le [DATE_NAISSANCE]"
        self._mask_preview.configure(text=text)

    # -- éditeur masques --------------------------------------------------


    def _open_full_mask_editor(self) -> None:
        existing = self._mask_editor_window
        if existing is not None:
            try:
                if existing.winfo_exists():
                    existing.lift()
                    existing.focus_force()
                    return
            except Exception:
                pass
        try:
            from gui_v6.mask_editor_window import MaskEditorWindow

            active = self._state.manual_mask_template
            initial_template = active if (active and Path(active).exists()) else None
            win = MaskEditorWindow(
                self.winfo_toplevel(),
                templates_dir=ensure_mask_templates_dir(_app_base_dir()),
                initial_template=initial_template,
                on_template_saved=self._on_mask_template_saved,
            )
            self._mask_editor_window = win
            self._mask_status_text.set("Éditeur de masques ouvert.")
        except Exception as exc:
            messagebox.showerror("Masques PDF", f"Impossible d'ouvrir l'éditeur : {exc}")

    def _on_mask_template_saved(self, path: Path) -> None:
        """Callback déclenché par la fenêtre dédiée après sauvegarde d'un template.

        Lie le template au profil en cours d'édition (`preferred_manual_mask_template`).
        """
        self._refresh_manual_mask_templates()
        try:
            self._manual_mask_var.set(mask_template_label(path, _app_base_dir()))
            self._state.manual_mask_template = path
        except Exception:
            pass
        # Renseigne le champ Template du profil édité (section Profils > Masquage).
        if hasattr(self, "_pro_template_var"):
            try:
                self._pro_template_var.set(str(path))
            except Exception:
                pass
        self._mask_status_text.set(f"Template enregistré : {path.name}")






















    # -- helpers UI -------------------------------------------------------

    def _columns(self, parent, count: int, gap: int = 8, height: int | None = None) -> list[ctk.CTkFrame]:
        row = ctk.CTkFrame(parent, fg_color="transparent")
        row.pack(fill="x")
        if height is not None:
            row.configure(height=height)
            row.pack_propagate(False)
        row.grid_rowconfigure(0, weight=1)
        frames: list[ctk.CTkFrame] = []
        for idx in range(count):
            row.grid_columnconfigure(idx, weight=1, uniform="config-cols")
            frame = ctk.CTkFrame(row, fg_color="transparent")
            frame.grid(row=0, column=idx, sticky="nsew", padx=(0 if idx == 0 else gap // 2, 0 if idx == count - 1 else gap // 2))
            frames.append(frame)
        return frames

    def _note(self, parent, text: str) -> None:
        p = self._p
        ctk.CTkLabel(
            parent,
            text=text,
            text_color=p["text_muted"],
            fg_color=p["divider"],
            corner_radius=4,
            font=ui_kit.font(11),
            anchor="w",
            justify="left",
            wraplength=330,
        ).pack(fill="x", padx=12, pady=(0, 10), ipady=5)

    def _mini_toggle(self, parent, label: str, hint: str, value: bool = True, variable=None,
                     command=None, disabled: bool = False, disabled_hint: str | None = None):
        p = self._p
        row = ctk.CTkFrame(parent, fg_color="transparent", height=MINI_TOGGLE_HEIGHT)
        row.pack_propagate(False)
        left = ctk.CTkFrame(row, fg_color="transparent")
        left.pack(side="left", fill="both", expand=True, pady=(3, 2))
        lbl_color = p["text_muted"] if disabled else p["text"]
        ctk.CTkLabel(
            left,
            text=label,
            text_color=lbl_color,
            font=ui_kit.font(MINI_TOGGLE_LABEL_FONT_SIZE, "bold"),
            anchor="w",
        ).pack(anchor="w")
        shown_hint = disabled_hint if (disabled and disabled_hint) else hint
        if shown_hint:
            ctk.CTkLabel(
                left,
                text=shown_hint,
                text_color=p["text_muted"] if disabled else p["text_dim"],
                font=ui_kit.font(MINI_TOGGLE_HINT_FONT_SIZE),
                anchor="w",
            ).pack(anchor="w", pady=(1, 0))
        # Moteur indisponible : on force l'état à False (jamais « coché mais absent »).
        if disabled and variable is None:
            value = False
        var = variable if variable is not None else ctk.BooleanVar(value=value)
        if disabled:
            var.set(False)
        switch = ctk.CTkSwitch(row, text="", variable=var, command=command, progress_color=p["primary"], width=38)
        if disabled:
            switch.configure(state="disabled")
        switch.pack(side="right", padx=(8, 0), pady=(8, 0))
        row.var = var  # type: ignore[attr-defined]
        row.switch = switch  # type: ignore[attr-defined]
        row.get = lambda: bool(var.get())  # type: ignore[attr-defined]
        return row

    def _slider_row(self, parent, label: str, variable: ctk.IntVar, command) -> None:
        p = self._p
        row = ctk.CTkFrame(parent, fg_color="transparent")
        row.pack(fill="x", padx=12, pady=2)
        ctk.CTkLabel(row, text=label, text_color=p["text"], font=ui_kit.font(12)).pack(side="left")
        ctk.CTkLabel(row, textvariable=variable, text_color=p["primary"], font=ui_kit.font(11, "bold"), width=20).pack(
            side="right"
        )
        slider = ctk.CTkSlider(row, from_=0, to=6, number_of_steps=6, progress_color=p["primary"], width=96, command=command)
        slider.set(variable.get())
        slider.pack(side="right", padx=(8, 4))

    def _rule_row(self, parent, values: tuple[str, str, str, str]) -> None:
        p = self._p
        label, rule_type, target, status = values
        row = ctk.CTkFrame(parent, fg_color="transparent")
        row.pack(fill="x", padx=12, pady=1)
        ctk.CTkLabel(row, text=label, width=190, anchor="w", text_color=p["text"], font=ui_kit.font(12)).pack(side="left")
        ctk.CTkLabel(row, text=rule_type, width=80, anchor="w", text_color=p["text_dim"], font=ui_kit.font(11)).pack(side="left")
        ctk.CTkLabel(row, text=target, width=210, anchor="w", text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="left")
        color = p["success"] if status == "Actif" else p["warning"]
        ctk.CTkLabel(row, text=status, width=70, anchor="w", text_color=color, font=ui_kit.font(11, "bold")).pack(side="left")
        self._mockup_button(row, "▶ Tester").pack(side="left")
