"""
Interface GUI moderne et améliorée avec onglets pour Web Sentinel
Design moderne avec positionnements corrects et esthétique soignée
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import logging
import threading
import json
import webbrowser
import os
from collections import Counter
from pathlib import Path
from typing import List, Dict, Any, Optional
import datetime

try:
    from PIL import Image, ImageTk
except ImportError:  # pragma: no cover - optional dependency
    Image = ImageTk = None

from ..model import ScanRequest, ScanResult, Finding
from ..scanner import SentinelScanner
from ..checks.source_code.scanner import SourceScanConfig
from ..reporting import HistoryStore, ReportEngine
from ..subscription import SubscriptionManager, SubscriptionTier
from ..subscription.validator import LicenseError as SubscriptionLicenseError, LicenseValidator
from ..auth import AuthManager, LoginDialog
from ..license.license_manager import LicenceError as EmbeddedLicenceError
from ..license.runtime_bridge import get_license_bridge, initialize_license_system
from .i18n import localization
from .invasive_warning import show_invasive_warning
from .license_dialog import LicenceOperationError, show_license_status_dialog
from .sections import LicenseSection, SASTSection, HistorySection, PaymentSection
from .theme_applicator import ThemeApplicator
from .theme_selector import show_theme_selector
from .themes import ThemeManager
from .themes_gui.shared import get_data_manager

LOGGER = logging.getLogger(__name__)

class _GUIStatusHandler(logging.Handler):
    "Forward log records to the GUI status bar."

    def __init__(self, gui: "ModernWebSentinelGUI") -> None:
        super().__init__()
        self.gui = gui

    def emit(self, record: logging.LogRecord) -> None:
        message = self.format(record)
        try:
            self.gui.root.after(0, self._update_status, message)
        except Exception:
            pass

    def _update_status(self, message: str) -> None:
        gui = self.gui
        if hasattr(gui, "status_var"):
            gui.status_var.set(message)



class ModernWebSentinelGUI:
    """Interface Web Sentinel moderne avec design amélioré et onglets fonctionnels."""
    
    def __init__(
        self,
        subscription_tier: str | SubscriptionTier | None = None,
        initial_theme: str | None = None,
    ):
        self.root = tk.Tk()
        self.root.title(localization.t("app_title"))
        self.root.geometry("1400x900")
        self.root.minsize(1200, 700)
        self.root.configure(bg='#f8fafc')
        
        # Gestionnaire de configuration GUI (thème, langue, etc.)
        self.data_manager = get_data_manager()
        self.style = ttk.Style()
        self.current_theme = self._normalize_theme_name(
            initial_theme or self.data_manager.get_gui_theme()
        )
        
        # Variables communes
        self.current_scan_thread = None
        self.current_sast_thread = None
        self.scan_results = []
        self.is_scanning = False
        self.invasive_confirmed = False
        
        # Système d'authentification et de licence
        self.auth_manager = AuthManager()
        self.license_bridge = get_license_bridge()
        self.license_initialized = initialize_license_system()
        self.license_section = LicenseSection(self)
        self.history_section = HistorySection(self)
        self.payment_section = PaymentSection(self)
        self.sast_section = SASTSection(self)
        self._status_log_handler = None

        logging.getLogger("web_sentinel").setLevel(logging.INFO)
        self._synchronize_license_with_auth()
        self.subscription_manager = self.license_bridge.get_runtime_manager()
        self.license_validator = self.license_bridge.get_runtime_validator()
        self.license_status = self.license_bridge.get_license_status()
        self.history_store = HistoryStore(Path.home() / ".web-sentinel" / "history.json")
        self.report_engine = ReportEngine(history_store=self.history_store)
        self.history_entries: List[ScanResult] = []
        self.sast_results: List[ScanResult] = []
        self.history_tree_data: Dict[str, ScanResult] = {}
        self._load_existing_history(limit=25)
        self._expected_sast_files: Optional[int] = None
        self._last_sast_root: Optional[Path] = None
        self.sast_logo_image = None
        self.language_combo: Optional[ttk.Combobox] = None
        self.language_code_map: Dict[str, str] = localization.get_available_languages()
        self._localized_widgets: List[tuple[Any, str]] = []
        self._status_key: Optional[str] = None
        self._status_kwargs: Dict[str, Any] = {}


        # Configuration
        self.config_file = Path.home() / ".web-sentinel" / "gui-config.json"
        self.load_config()
        
        # Créer l'interface moderne
        self.setup_modern_ui()
        self.load_domains_from_config()
        self.refresh_subscription_ui(self.license_status)
        self.refresh_localized_content()
        self._apply_theme(self.current_theme)
        
        # Gestion de la fermeture
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
    
    def setup_modern_style(self, palette: dict[str, str]):
        """Configure les styles ttk en fonction de la palette active."""
        style = ttk.Style()
        style.theme_use('clam')
        
        colors = {
            'primary': palette['accent'],
            'secondary': palette['text_secondary'],
            'success': palette['success'],
            'warning': palette['warning'],
            'danger': palette['danger'],
            'background': palette['background'],
            'surface': palette['surface'],
            'border': palette['border'],
            'text_primary': palette['text_primary'],
            'text_secondary': palette['text_secondary'],
        }
        
        # Style pour les onglets
        style.configure('Modern.TNotebook', 
                       background=colors['background'],
                       borderwidth=0,
                       tabmargins=[2, 5, 2, 0])
        
        style.configure('Modern.TNotebook.Tab',
                       background=colors['surface'],
                       foreground=colors['secondary'],
                       padding=[20, 10],
                       borderwidth=1,
                       relief='solid')
        
        style.map('Modern.TNotebook.Tab',
                 background=[('selected', colors['primary']),
                            ('active', colors['border'])],
                 foreground=[('selected', 'white'),
                            ('active', colors['primary'])])
        
        # Style pour les frames
        style.configure('Modern.TFrame',
                       background=colors['surface'],
                       borderwidth=1,
                       relief='solid')
        
        # Style pour les boutons
        style.configure('Modern.TButton',
                       background=colors['primary'],
                       foreground='white',
                       borderwidth=0,
                       focuscolor='none',
                       padding=[20, 10])
        
        style.map('Modern.TButton',
                 background=[('active', colors['primary']),
                            ('pressed', colors['border'])])
        
        # Style pour les boutons secondaires
        style.configure('Secondary.TButton',
                       background=colors['surface'],
                       foreground=colors['secondary'],
                       borderwidth=1,
                       relief='solid',
                       padding=[20, 10])
        
        # Style pour les labels
        style.configure('Modern.TLabel',
                       background=colors['surface'],
                       foreground=colors['secondary'])
        
        style.configure('Title.TLabel',
                       background=colors['surface'],
                       foreground=colors['text_primary'],
                       font=('Segoe UI', 12, 'bold'))
        
        style.configure('Subtitle.TLabel',
                       background=colors['surface'],
                       foreground=colors['secondary'],
                       font=('Segoe UI', 10))
    
    def _normalize_theme_name(self, theme_name: str | None) -> str:
        if not theme_name:
            return "default"
        value = str(theme_name).strip().lower()
        return ThemeApplicator.THEME_ALIASES.get(value, value)
    
    def _get_theme_palette(self, theme_name: str) -> dict[str, str]:
        theme = ThemeManager.get_theme(self._normalize_theme_name(theme_name)) or ThemeManager.get_theme("default")
        colors = theme.colors
        return {
            "background": colors.bg_primary,
            "surface": colors.bg_secondary,
            "text_primary": colors.fg_primary,
            "text_secondary": colors.fg_secondary,
            "accent": colors.bg_accent,
            "border": colors.border,
            "success": colors.success,
            "warning": colors.warning,
            "danger": colors.error,
        }
    
    def _apply_theme(self, theme_name: str, persist: bool = False) -> None:
        normalized = self._normalize_theme_name(theme_name)
        ThemeApplicator.apply_theme(self.root, normalized)
        palette = self._get_theme_palette(normalized)
        self.setup_modern_style(palette)
        self.current_theme = normalized
        
        if persist:
            self.data_manager.set_gui_theme(normalized)
    
    def setup_modern_ui(self):
        """Créer l'interface utilisateur moderne."""
        
        # Container principal avec padding
        main_container = ttk.Frame(self.root, style='Modern.TFrame')
        main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
        
        # En-tête moderne
        self.create_modern_header(main_container)
        
        # Séparateur
        separator = ttk.Separator(main_container, orient='horizontal')
        separator.pack(fill=tk.X, pady=15)
        
        # Zone des onglets
        self.create_modern_tabs(main_container)
        
        # Barre de statut moderne
        self.create_modern_status_bar(main_container)
        
        # Vérifier les permissions
        self.update_tab_permissions()
    
    def create_modern_header(self, parent):
        """Créer un en-tête moderne avec gradient visuel."""
        header_frame = ttk.Frame(parent, style='Modern.TFrame')
        header_frame.pack(fill=tk.X, pady=(0, 10))
        
        # Titre principal avec icône (gauche)
        title_frame = ttk.Frame(header_frame, style='Modern.TFrame')
        title_frame.pack(side=tk.LEFT, fill=tk.Y)
        
        title_label = ttk.Label(title_frame, 
                               text=self._text("brand_title"), 
                               font=('Segoe UI', 24, 'bold'),
                               foreground='#1f2937')
        title_label.pack(anchor=tk.W)
        self._register_widget(title_label, "brand_title")
        
        self.subtitle_label = ttk.Label(title_frame,
                                        font=('Segoe UI', 11),
                                        foreground='#6b7280')
        self.subtitle_label.pack(anchor=tk.W, pady=(0, 5))
        self._register_widget(self.subtitle_label, "header_subtitle")
        
        # Section utilisateur moderne (droite)
        self.user_frame = ttk.Frame(header_frame, style='Modern.TFrame')
        self.user_frame.pack(side=tk.RIGHT, fill=tk.Y)
        self._create_modern_user_section()
        
        # Section centrale avec l'image banner
        self.banner_frame = ttk.Frame(header_frame, style='Modern.TFrame')
        self.banner_frame.pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=20)
        self._create_banner_section()
    
    def _create_modern_user_section(self):
        """Créer la section utilisateur moderne."""
        for widget in self.user_frame.winfo_children():
            widget.destroy()
        
        if self.auth_manager.is_authenticated():
            user = self.auth_manager.get_current_user()
            if user:
                # Carte utilisateur moderne
                user_card = ttk.Frame(self.user_frame, style='Modern.TFrame', padding=15)
                user_card.pack(side=tk.RIGHT)
                
                user_info = ttk.Label(user_card, 
                                     text=f"👤 {user.display_name}",
                                     font=('Segoe UI', 11, 'bold'),
                                     foreground='#1f2937')
                user_info.pack(anchor=tk.E)
                
                tier_text = self.subscription_manager.tier.value.upper()
                tier_colors = {
                    "FREE": "#6b7280", 
                    "PRO": "#2563eb", 
                    "ENTERPRISE": "#7c3aed", 
                    "SYSOP": "#dc2626"
                }
                
                tier_label = ttk.Label(user_card,
                                      text=f"🏷️ {tier_text}",
                                      font=('Segoe UI', 10),
                                      foreground=tier_colors.get(tier_text, "#6b7280"))
                tier_label.pack(anchor=tk.E)
                
                # Boutons d'action
                button_frame = ttk.Frame(user_card)
                button_frame.pack(fill=tk.X, pady=(10, 0))
                
                # Bouton changement de thème
                theme_btn = ttk.Button(
                    button_frame,
                    text="🎨 Thème",
                    command=self.change_theme,
                    style='Secondary.TButton',
                )
                theme_btn.pack(side=tk.RIGHT, padx=(5, 0))
                
                license_btn = ttk.Button(
                    button_frame,
                    command=self.show_license_status,
                    style='Secondary.TButton',
                )
                license_btn.pack(side=tk.RIGHT, padx=(5, 0))
                self._register_widget(license_btn, "user_license_button")
                
                logout_btn = ttk.Button(
                    button_frame,
                    command=self.logout,
                    style='Secondary.TButton',
                )
                logout_btn.pack(side=tk.RIGHT)
                self._register_widget(logout_btn, "user_logout_button")
        else:
            # Bouton de connexion moderne
            login_btn = ttk.Button(
                self.user_frame,
                command=self.show_login,
                style='Modern.TButton',
            )
            login_btn.pack(side=tk.RIGHT)
            self._register_widget(login_btn, "user_login_button")
    
    def _create_banner_section(self):
        """Créer la section centrale avec l'image banner."""
        try:
            from PIL import Image, ImageTk
            from pathlib import Path
            
            # Chemin vers l'image
            image_path = Path("images") / "web-sentinel.png"
            
            if image_path.exists():
                # Charger l'image
                self.original_banner_image = Image.open(image_path)
                
                # Créer le label avec redimensionnement dynamique
                self.banner_label = ttk.Label(self.banner_frame)
                self.banner_label.pack(expand=True, fill=tk.BOTH)
                
                # Redimensionnement initial immédiat (sans animation)
                self._resize_banner_image()
                
                print(f"✅ Banner image chargée: {image_path}")
            else:
                # Image de secours avec texte
                fallback_label = ttk.Label(self.banner_frame, 
                                         text=self._text("brand_title_caps"), 
                                         font=('Segoe UI', 16, 'bold'),
                                         foreground='#2563eb')
                fallback_label.pack(expand=True)
                self._register_widget(fallback_label, "brand_title_caps")
                try:
                    print(f"⚠️ Image non trouvée: {image_path}")
                except UnicodeEncodeError:
                    print(f"WARNING: Image not found: {image_path}")
                
        except ImportError:
            # PIL non disponible, utiliser du texte
            fallback_label = ttk.Label(self.banner_frame, 
                                     text=self._text("brand_title_caps"), 
                                     font=('Segoe UI', 16, 'bold'),
                                     foreground='#2563eb')
            fallback_label.pack(expand=True)
            self._register_widget(fallback_label, "brand_title_caps")
            print(localization.t("messages.gui.pil_fallback"))
        except Exception as e:
            # Erreur générale, utiliser du texte
            fallback_label = ttk.Label(self.banner_frame, 
                                     text=self._text("brand_title_caps"), 
                                     font=('Segoe UI', 16, 'bold'),
                                     foreground='#2563eb')
            fallback_label.pack(expand=True)
            self._register_widget(fallback_label, "brand_title_caps")
            print(f"❌ Erreur lors du chargement de l'image: {e}")
    
    def _resize_banner_image(self, event=None):
        """Redimensionner l'image banner pour qu'elle remplisse l'espace disponible."""
        try:
            if hasattr(self, 'original_banner_image') and hasattr(self, 'banner_label'):
                # Obtenir les dimensions disponibles
                frame_width = self.banner_frame.winfo_width()
                frame_height = self.banner_frame.winfo_height()
                
                # Ignorer si les dimensions ne sont pas encore disponibles
                if frame_width <= 1 or frame_height <= 1:
                    return
                
                # Calculer les ratios pour adapter l'image tout en préservant l'aspect ratio
                width_ratio = frame_width / self.original_banner_image.width
                height_ratio = frame_height / self.original_banner_image.height
                
                # Utiliser le ratio le plus petit pour que l'image rentre entièrement
                ratio = min(width_ratio, height_ratio)
                
                # Calculer les nouvelles dimensions
                new_width = int(self.original_banner_image.width * ratio)
                new_height = int(self.original_banner_image.height * ratio)
                
                # Redimensionner l'image
                resized_image = self.original_banner_image.resize(
                    (new_width, new_height), 
                    Image.Resampling.LANCZOS
                )
                
                # Convertir pour tkinter et mettre à jour le label
                self.banner_photo = ImageTk.PhotoImage(resized_image)
                self.banner_label.configure(image=self.banner_photo)
                
                try:
                    print(f"🔄 Image redimensionnée: {new_width}×{new_height} (frame: {frame_width}×{frame_height})")
                except UnicodeEncodeError:
                    print(f"INFO: Image resized: {new_width}x{new_height} (frame: {frame_width}x{frame_height})")
                
        except Exception as e:
            try:
                print(f"❌ Erreur lors du redimensionnement: {e}")
            except UnicodeEncodeError:
                print(f"ERROR: Resize error: {e}")
    
    def create_modern_tabs(self, parent):
        """Créer les onglets avec design moderne."""
        # Container pour les onglets
        tab_container = ttk.Frame(parent, style='Modern.TFrame')
        tab_container.pack(fill=tk.BOTH, expand=True, pady=10)
        
        # Notebook avec style moderne
        self.notebook = ttk.Notebook(tab_container, style='Modern.TNotebook')
        self.notebook.pack(fill=tk.BOTH, expand=True)
        
        # Onglet 1: Scan de domaine
        self.domain_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.domain_tab, text=self._text("tab_domain_scan"))
        self.setup_modern_domain_tab()
        
        # Onglet 2: Analyse SAST
        self.sast_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.sast_tab, text=self._text("tab_sast_analysis"))
        self.setup_modern_sast_tab()
        
        # Onglet 3: Rapports
        self.history_section.build_tab()
        
        # Onglet 4: Configuration
        self.config_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.config_tab, text=self._text("tab_settings"))
        self.setup_modern_config_tab()
    
    def setup_modern_domain_tab(self):
        """Configuration de l'onglet scan de domaine avec design moderne."""
        # Conteneur principal avec grid
        main_frame = ttk.Frame(self.domain_tab, style='Modern.TFrame', padding=20)
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Configuration responsive
        main_frame.grid_columnconfigure(0, weight=1)
        main_frame.grid_columnconfigure(1, weight=2)
        main_frame.grid_rowconfigure(2, weight=1)
        
        # Section de configuration (gauche)
        self.domain_config_frame = ttk.LabelFrame(main_frame, padding=20)
        self.domain_config_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10), pady=(0, 10))
        self.domain_config_frame.grid_columnconfigure(0, weight=1)
        self._register_widget(self.domain_config_frame, "domain_config_frame")
        
        # Saisie de domaine moderne
        self.domain_input_label = ttk.Label(self.domain_config_frame, style='Title.TLabel')
        self.domain_input_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
        self._register_widget(self.domain_input_label, "domain_input_label")
        
        domain_frame = ttk.Frame(self.domain_config_frame)
        domain_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
        domain_frame.grid_columnconfigure(0, weight=1)
        
        self.domain_var = tk.StringVar()
        self.domain_entry = ttk.Entry(domain_frame, textvariable=self.domain_var, font=('Segoe UI', 11))
        self.domain_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
        
        add_domain_btn = ttk.Button(domain_frame, text="➕", command=self.add_domain_to_list, width=3)
        add_domain_btn.grid(row=0, column=1)
        
        # Liste des domaines
        self.domain_list_label = ttk.Label(self.domain_config_frame, style='Title.TLabel')
        self.domain_list_label.grid(row=2, column=0, sticky=tk.W, pady=(0, 5))
        self._register_widget(self.domain_list_label, "domain_list_label")
        
        list_frame = ttk.Frame(self.domain_config_frame)
        list_frame.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 15))
        list_frame.grid_columnconfigure(0, weight=1)
        list_frame.grid_rowconfigure(0, weight=1)
        
        self.domain_listbox = tk.Listbox(list_frame, height=5, font=('Segoe UI', 10))
        self.domain_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.domain_listbox.yview)
        scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.domain_listbox.configure(yscrollcommand=scrollbar.set)
        
        # Boutons d'action
        button_frame = ttk.Frame(self.domain_config_frame)
        button_frame.grid(row=4, column=0, sticky=(tk.W, tk.E))
        
        self.remove_domain_btn = ttk.Button(button_frame, command=self.remove_selected_domain)
        self.remove_domain_btn.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.remove_domain_btn, "domain_remove_button")
        
        self.clear_domains_btn = ttk.Button(button_frame, command=self.clear_domain_list)
        self.clear_domains_btn.pack(side=tk.LEFT)
        self._register_widget(self.clear_domains_btn, "domain_clear_button")
        
        # Options de scan
        self.domain_options_frame = ttk.LabelFrame(self.domain_config_frame, padding=15)
        self.domain_options_frame.grid(row=5, column=0, sticky=(tk.W, tk.E), pady=(15, 0))
        self._register_widget(self.domain_options_frame, "domain_options_frame")
        
        self.invasive_var = tk.BooleanVar()
        self.domain_invasive_text = tk.StringVar()
        invasive_check = ttk.Checkbutton(self.domain_options_frame, textvariable=self.domain_invasive_text, variable=self.invasive_var)
        invasive_check.pack(anchor=tk.W, pady=2)
        self._register_widget(self.domain_invasive_text, "domain_invasive_option")
        
        self.history_var = tk.BooleanVar(value=True)
        self.domain_history_text = tk.StringVar()
        history_check = ttk.Checkbutton(self.domain_options_frame, textvariable=self.domain_history_text, variable=self.history_var)
        history_check.pack(anchor=tk.W, pady=2)
        self._register_widget(self.domain_history_text, "domain_history_option")
        
        # Bouton de scan principal
        self.scan_button = ttk.Button(self.domain_config_frame,
                                      command=self.start_domain_scan,
                                      style='Modern.TButton')
        self.scan_button.grid(row=6, column=0, pady=(20, 0), sticky=(tk.W, tk.E))
        self._register_widget(self.scan_button, "domain_scan_button")
        
        # Section des résultats (droite)
        self.domain_results_frame = ttk.LabelFrame(main_frame, padding=20)
        self.domain_results_frame.grid(row=0, column=1, rowspan=3, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.domain_results_frame.grid_columnconfigure(0, weight=1)
        self.domain_results_frame.grid_rowconfigure(1, weight=1)
        self._register_widget(self.domain_results_frame, "domain_results_frame")
        
        # Barre de progression
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(self.domain_results_frame, variable=self.progress_var, mode='determinate')
        self.progress_bar.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
        
        # Zone de résultats avec scrollbar
        self.results_text = scrolledtext.ScrolledText(self.domain_results_frame, 
                                                     wrap=tk.WORD, 
                                                     font=('Consolas', 10),
                                                     bg='#1e293b',
                                                     fg='#e2e8f0',
                                                     insertbackground='#e2e8f0')
        self.results_text.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Boutons d'export
        export_frame = ttk.Frame(self.domain_results_frame)
        export_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(15, 0))
        
        self.export_json_button = ttk.Button(export_frame, command=self.export_json)
        self.export_json_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.export_json_button, "domain_export_json")

        self.export_html_button = ttk.Button(export_frame, command=self.export_html)
        self.export_html_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.export_html_button, "domain_export_html")

        self.export_ai_button = ttk.Button(export_frame, command=self.export_ai_analysis)
        self.export_ai_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.export_ai_button, "domain_export_ai")

        self.export_clear_button = ttk.Button(export_frame, command=self.clear_results)
        self.export_clear_button.pack(side=tk.RIGHT)
        self._register_widget(self.export_clear_button, "domain_export_clear")
    
    def setup_modern_sast_tab(self):
        """Configuration de l'onglet SAST avec design moderne."""
        # Conteneur principal
        main_frame = ttk.Frame(self.sast_tab, style='Modern.TFrame', padding=20)
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        main_frame.grid_columnconfigure(0, weight=1)
        main_frame.grid_columnconfigure(1, weight=2)
        main_frame.grid_rowconfigure(2, weight=1)
        
        # Section de sélection de fichiers (gauche)
        selection_frame = ttk.LabelFrame(main_frame, padding=20)
        selection_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10), pady=(0, 10))
        selection_frame.grid_columnconfigure(0, weight=1)
        self._register_widget(selection_frame, "sast_selection_frame")
        
        # Informations sur les permissions SAST
        self.sast_info_frame = ttk.Frame(selection_frame, style='Modern.TFrame')
        self.sast_info_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
        self._update_sast_info()
        
        # Sélection de dossier
        self.sast_folder_label = ttk.Label(selection_frame, style='Title.TLabel')
        self.sast_folder_label.grid(row=1, column=0, sticky=tk.W, pady=(0, 5))
        self._register_widget(self.sast_folder_label, "sast_folder_label")
        
        folder_frame = ttk.Frame(selection_frame)
        folder_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        folder_frame.grid_columnconfigure(0, weight=1)
        
        self.folder_var = tk.StringVar()
        self.folder_entry = ttk.Entry(folder_frame, textvariable=self.folder_var, font=('Segoe UI', 10))
        self.folder_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
        
        self.browse_folder_btn = ttk.Button(folder_frame, command=self.browse_folder)
        self.browse_folder_btn.grid(row=0, column=1)
        self._register_widget(self.browse_folder_btn, "folder_browse")
        
        # Sélection de fichiers individuels
        self.sast_manual_label = ttk.Label(selection_frame, style='Title.TLabel')
        self.sast_manual_label.grid(row=3, column=0, sticky=tk.W, pady=(15, 5))
        self._register_widget(self.sast_manual_label, "sast_manual_label")
        
        files_frame = ttk.Frame(selection_frame)
        files_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        files_frame.grid_columnconfigure(0, weight=1)
        
        self.browse_files_btn = ttk.Button(files_frame, command=self.browse_files)
        self.browse_files_btn.grid(row=0, column=0, sticky=tk.W)
        self._register_widget(self.browse_files_btn, "sast_select_files_button")
        
        # Liste des fichiers sélectionnés
        self.sast_files_label = ttk.Label(selection_frame, style='Title.TLabel')
        self.sast_files_label.grid(row=5, column=0, sticky=tk.W, pady=(15, 5))
        self._register_widget(self.sast_files_label, "sast_files_label")
        
        files_list_frame = ttk.Frame(selection_frame)
        files_list_frame.grid(row=6, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 15))
        files_list_frame.grid_columnconfigure(0, weight=1)
        files_list_frame.grid_rowconfigure(0, weight=1)
        
        self.files_listbox = tk.Listbox(files_list_frame, height=6, font=('Segoe UI', 9))
        self.files_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        files_scrollbar = ttk.Scrollbar(files_list_frame, orient=tk.VERTICAL, command=self.files_listbox.yview)
        files_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.files_listbox.configure(yscrollcommand=files_scrollbar.set)
        
        # Boutons de gestion des fichiers
        files_button_frame = ttk.Frame(selection_frame)
        files_button_frame.grid(row=7, column=0, sticky=(tk.W, tk.E))
        
        self.sast_remove_file_button = ttk.Button(files_button_frame, command=self.remove_selected_file)
        self.sast_remove_file_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.sast_remove_file_button, "sast_remove_file_button")

        self.sast_clear_files_button = ttk.Button(files_button_frame, command=self.clear_files_list)
        self.sast_clear_files_button.pack(side=tk.LEFT)
        self._register_widget(self.sast_clear_files_button, "sast_clear_files_button")
        
        # Options SAST
        self.sast_options_frame = ttk.LabelFrame(selection_frame, padding=15)
        self.sast_options_frame.grid(row=8, column=0, sticky=(tk.W, tk.E), pady=(15, 0))
        self._register_widget(self.sast_options_frame, "sast_options_frame")
        
        self.sast_recursive_var = tk.BooleanVar(value=True)
        self.sast_recursive_text = tk.StringVar()
        recursive_check = ttk.Checkbutton(self.sast_options_frame, textvariable=self.sast_recursive_text,
                                          variable=self.sast_recursive_var,
                                          command=self.on_recursive_changed)
        recursive_check.pack(anchor=tk.W, pady=2)
        self._register_widget(self.sast_recursive_text, "sast_recursive")

        self.sast_detailed_var = tk.BooleanVar()
        self.sast_detailed_text = tk.StringVar()
        detailed_check = ttk.Checkbutton(self.sast_options_frame, textvariable=self.sast_detailed_text, variable=self.sast_detailed_var)
        detailed_check.pack(anchor=tk.W, pady=2)
        self._register_widget(self.sast_detailed_text, "sast_detailed")
        
        # Bouton de scan SAST
        self.sast_scan_button = ttk.Button(selection_frame,
                                           command=self.start_sast_scan,
                                           style='Modern.TButton')
        self.sast_scan_button.grid(row=9, column=0, pady=(20, 0), sticky=(tk.W, tk.E))
        self._register_widget(self.sast_scan_button, "sast_scan_button")
        
        # Section des résultats SAST (droite)
        sast_results_frame = ttk.LabelFrame(main_frame, padding=20)
        sast_results_frame.grid(row=0, column=1, rowspan=3, sticky=(tk.W, tk.E, tk.N, tk.S))
        sast_results_frame.grid_columnconfigure(0, weight=1)
        sast_results_frame.grid_rowconfigure(2, weight=1)  # Ajusté pour supprimer l'espace du logo
        self._register_widget(sast_results_frame, "sast_results_frame")

        # Statistiques SAST
        self.sast_stats_frame = ttk.Frame(sast_results_frame)
        self.sast_stats_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15))

        # Barre de progression SAST
        self.sast_progress_var = tk.DoubleVar()
        self.sast_progress_bar = ttk.Progressbar(sast_results_frame, variable=self.sast_progress_var, mode='determinate')
        self.sast_progress_bar.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 15))

        # Zone de résultats SAST
        self.sast_results_text = scrolledtext.ScrolledText(sast_results_frame,
                                                          wrap=tk.WORD,
                                                          font=('Consolas', 10),
                                                          bg='#1e293b',
                                                          fg='#e2e8f0',
                                                          insertbackground='#e2e8f0')
        self.sast_results_text.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # Boutons d'export SAST
        sast_export_frame = ttk.Frame(sast_results_frame)
        sast_export_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(15, 0))

        self.export_sast_json_button = ttk.Button(sast_export_frame, command=self.export_sast_json)
        self.export_sast_json_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.export_sast_json_button, "export_json_btn")

        self.export_sast_html_button = ttk.Button(sast_export_frame, command=self.export_sast_html)
        self.export_sast_html_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.export_sast_html_button, "export_html_btn")

        self.export_sast_ai_button = ttk.Button(sast_export_frame, command=self.export_sast_ai_analysis)
        self.export_sast_ai_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.export_sast_ai_button, "export_ai_btn")

        self.export_sast_copy_button = ttk.Button(sast_export_frame, command=self.copy_sast_results)
        self.export_sast_copy_button.pack(side=tk.LEFT, padx=(0, 10))
        self._register_widget(self.export_sast_copy_button, "export_copy_btn")

        self.export_sast_clear_button = ttk.Button(sast_export_frame, command=self.clear_sast_results)
        self.export_sast_clear_button.pack(side=tk.RIGHT)
        self._register_widget(self.export_sast_clear_button, "export_clear_btn")
    
    def open_checkout(self, tier: str, cadence: str) -> Optional[str]:
        """Déclencher le checkout Stripe via la section paiement."""
        try:
            url = self.payment_section.open_checkout(tier, cadence)
            if url:
                LOGGER.info("Checkout initialised for tier=%s cadence=%s", tier, cadence)
                return url
            messagebox.showwarning(self._text("attention_title"), self._text("subscription_payment_unavailable"))
        except Exception as exc:  # pragma: no cover - garde-fou
            LOGGER.exception("Failed to open checkout")
            messagebox.showerror(self._text("error_title"), str(exc))
        return None

    def setup_modern_config_tab(self):
        """Configuration de l'onglet configuration avec design moderne."""
        main_frame = ttk.Frame(self.config_tab, style='Modern.TFrame', padding=20)
        main_frame.pack(fill=tk.BOTH, expand=True)

        self.config_title_label = ttk.Label(main_frame, style='Title.TLabel')
        self.config_title_label.pack(anchor=tk.W, pady=(0, 20))
        self._register_widget(self.config_title_label, "config_title")

        account_frame = ttk.LabelFrame(main_frame, padding=15)
        account_frame.pack(fill=tk.X, pady=(0, 15))
        self._register_widget(account_frame, "account_frame_title")

        self.account_status_var = tk.StringVar(value=self._text("account_status_guest"))
        account_label = ttk.Label(account_frame, textvariable=self.account_status_var, font=('Segoe UI', 10, 'bold'))
        account_label.grid(row=0, column=0, sticky=tk.W)

        self.account_action_button = ttk.Button(account_frame, command=self.show_login)
        self.account_action_button.grid(row=0, column=1, padx=(10, 0))
        self._register_widget(self.account_action_button, "account_login")

        self.logout_button_config = ttk.Button(account_frame, command=self.logout)
        self.logout_button_config.grid(row=0, column=2, padx=(10, 0))
        self._register_widget(self.logout_button_config, "account_logout")

        license_frame = ttk.LabelFrame(main_frame, padding=15)
        license_frame.pack(fill=tk.X, pady=(0, 15))
        self._register_widget(license_frame, "license_frame_title")

        self.license_tier_var = tk.StringVar()
        ttk.Label(license_frame, textvariable=self.license_tier_var, font=('Segoe UI', 10, 'bold')).grid(row=0, column=0, sticky=tk.W)

        self.license_limits_var = tk.StringVar()
        ttk.Label(license_frame, textvariable=self.license_limits_var, font=('Segoe UI', 10), foreground='#475569').grid(row=1, column=0, sticky=tk.W, pady=(5, 0))

        self.license_features_label = ttk.Label(license_frame, justify=tk.LEFT, font=('Segoe UI', 10))
        self.license_features_label.grid(row=2, column=0, sticky=tk.W, pady=(10, 0))

        self.manage_license_button = ttk.Button(license_frame, command=self.show_license_status)
        self.manage_license_button.grid(row=0, column=1, padx=(15, 0))
        self._register_widget(self.manage_license_button, "manage_license")

        language_frame = ttk.LabelFrame(main_frame, padding=15)
        language_frame.pack(fill=tk.X, pady=(0, 15))
        self._register_widget(language_frame, "language_section")

        self.language_label = ttk.Label(language_frame)
        self.language_label.grid(row=0, column=0, sticky=tk.W)
        self._register_widget(self.language_label, "language_label")

        self.language_combo = ttk.Combobox(language_frame, state="readonly", width=22)
        self.language_combo.grid(row=0, column=1, padx=(10, 0))
        self.language_combo.bind('<<ComboboxSelected>>', self._on_language_change)
        self._update_language_selector()

        preferences_frame = ttk.LabelFrame(main_frame, padding=15)
        preferences_frame.pack(fill=tk.X, pady=(0, 15))
        self._register_widget(preferences_frame, "preferences_frame")

        self.pref_history_var = tk.StringVar()
        ttk.Checkbutton(
            preferences_frame,
            textvariable=self.pref_history_var,
            variable=self.history_var,
            command=self.save_config,
        ).grid(row=0, column=0, sticky=tk.W)
        self._register_widget(self.pref_history_var, "pref_history")

        self.pref_recursive_var = tk.StringVar()
        ttk.Checkbutton(
            preferences_frame,
            textvariable=self.pref_recursive_var,
            variable=self.sast_recursive_var,
            command=self.save_config,
        ).grid(row=1, column=0, sticky=tk.W, pady=(5, 0))
        self._register_widget(self.pref_recursive_var, "pref_recursive")

        self.pref_detailed_var = tk.StringVar()
        ttk.Checkbutton(
            preferences_frame,
            textvariable=self.pref_detailed_var,
            variable=self.sast_detailed_var,
            command=self.save_config,
        ).grid(row=2, column=0, sticky=tk.W, pady=(5, 0))
        self._register_widget(self.pref_detailed_var, "pref_detailed")

        self.pref_invasive_var = tk.StringVar()
        ttk.Checkbutton(
            preferences_frame,
            textvariable=self.pref_invasive_var,
            variable=self.invasive_var,
            command=self.save_config,
        ).grid(row=3, column=0, sticky=tk.W, pady=(5, 0))
        self._register_widget(self.pref_invasive_var, "pref_invasive")

        self.refresh_configuration_tab()

    def refresh_configuration_tab(self) -> None:
        """Mettre à jour les informations du panneau de configuration."""
        if not hasattr(self, "account_status_var"):
            return

        if self.auth_manager.is_authenticated():
            user = self.auth_manager.get_current_user()
            display_name = getattr(user, "display_name", getattr(user, "email", self._text("account_default_name")))
            self.account_status_var.set(self._text("account_status_user", user=display_name))
            self.account_action_button.config(text=self._text("account_switch"), state=tk.NORMAL)
            self.logout_button_config.config(text=self._text("account_logout"), state=tk.NORMAL)
        else:
            self.account_status_var.set(self._text("account_status_guest"))
            self.account_action_button.config(text=self._text("account_login"), state=tk.NORMAL)
            self.logout_button_config.config(text=self._text("account_logout"), state=tk.DISABLED)

        self.manage_license_button.config(text=self._text("manage_license"))

        features = self._get_runtime_features()
        tier_attr = getattr(features, "tier", self._resolve_subscription_tier())
        if isinstance(tier_attr, SubscriptionTier):
            tier_label = tier_attr.value.upper()
        else:
            tier_label = str(tier_attr).upper()
        self.license_tier_var.set(self._text("license_current", tier=tier_label))

        domain_limit = self._format_limit_value(getattr(features, "domain_limit", -1), unlimited_key="unlimited_domains")
        sast_limit = self._format_limit_value(getattr(features, "max_source_files", -1), unlimited_key="unlimited_files")
        size_limit = self._format_size_value(getattr(features, "max_source_size_mb", -1))
        self.license_limits_var.set(
            self._text("license_limits", domains=domain_limit, files=sast_limit, size=size_limit)
        )

        status_symbol = self._status_symbol
        features_lines = [
            self._text("license_feature_domains", status=status_symbol(True)),
            self._text("license_feature_invasive", status=status_symbol(getattr(features, "allow_invasive_tests", False))),
            self._text("license_feature_html", status=status_symbol(getattr(features, "allow_html_export", False))),
            self._text("license_feature_api", status=status_symbol(getattr(features, "allow_api_access", False))),
            self._text("license_feature_sast", status=status_symbol(getattr(features, "allow_source_scan", False))),
            self._text("license_feature_advanced", status=status_symbol(getattr(features, "advanced_rules", False))),
        ]
        self.license_features_label.config(text="\n".join(features_lines))

        self._update_language_selector()
    
    def create_modern_status_bar(self, parent):
        """Créer une barre de statut moderne."""
        status_frame = ttk.Frame(parent, style='Modern.TFrame', padding=10)
        status_frame.pack(fill=tk.X, side=tk.BOTTOM)
        
        self.status_var = tk.StringVar()
        self._set_status("status_ready")
        status_label = ttk.Label(status_frame, textvariable=self.status_var, 
                                font=('Segoe UI', 10), foreground='#059669')
        status_label.pack(side=tk.LEFT)

        if self._status_log_handler is None:
            handler = _GUIStatusHandler(self)
            handler.setLevel(logging.INFO)
            handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
            logging.getLogger("web_sentinel").addHandler(handler)
            self._status_log_handler = handler

        # Indicateur de connexion
        self.connection_status_var = tk.StringVar(value=self._text("connection_offline"))
        connection_label = ttk.Label(status_frame, textvariable=self.connection_status_var,
                                   font=('Segoe UI', 10), foreground='#dc2626')
        connection_label.pack(side=tk.RIGHT)
    
    # Méthodes de synchronisation et gestion de licence (reprises de l'ancienne version)
    def _synchronize_license_with_auth(self):
        """Synchroniser le système de licence avec l'authentification."""
        self.license_section.synchronize_with_auth()

    def _refresh_license_state(self):
        """Récupérer les objets runtime dépendants de la licence."""
        self.license_section.refresh_state()

    def _resolve_subscription_tier(self) -> SubscriptionTier:
        """Determine the active subscription tier with graceful fallbacks."""
        return self.license_section.resolve_subscription_tier()

    def _get_runtime_features(self):
        """Return a mutable view of the current feature set."""
        return self.license_section.get_runtime_features()
    
    def update_tab_permissions(self):
        """Mettre à jour les permissions des onglets selon la licence."""
        if not hasattr(self, "notebook"):
            return
        if not self._can_use_sast():
            # Désactiver l'onglet SAST si pas de permission
            self.notebook.tab(1, state='disabled')
        else:
            self.notebook.tab(1, state='normal')
    
    def _can_use_sast(self) -> bool:
        """Vérifier si l'utilisateur peut utiliser SAST."""
        return self.sast_section.can_use_sast()

    def _update_sast_info(self):
        """Mettre à jour les informations SAST selon les permissions."""
        self.sast_section.update_info()

    def _update_connection_status(self):
        """Mettre à jour l'état de connexion dans la barre de statut."""
        if hasattr(self, "connection_status_var"):
            status = self._text("connection_online") if self.auth_manager.is_authenticated() else self._text("connection_offline")
            self.connection_status_var.set(status)

    def _load_brand_image(self, target_size: tuple[int, int] = (360, 180)):
        """Charger et redimensionner le logo de Web Sentinel."""
        if self.sast_logo_image is not None:
            return self.sast_logo_image

        base_dir = Path(__file__).resolve().parent.parent
        image_path = base_dir.parent / "images" / "web_sentinel.png"

        if Image and ImageTk and image_path.exists():
            try:
                image = Image.open(image_path)
                image = image.resize(target_size, Image.LANCZOS)
                self.sast_logo_image = ImageTk.PhotoImage(image)
                return self.sast_logo_image
            except Exception:
                self.sast_logo_image = None

        if image_path.exists():
            try:
                photo = tk.PhotoImage(file=str(image_path))
                # Simple downscale if needed
                factor = max(photo.width() / target_size[0], photo.height() / target_size[1], 1)
                if factor > 1:
                    photo = photo.subsample(int(factor))
                self.sast_logo_image = photo
                return self.sast_logo_image
            except Exception:
                self.sast_logo_image = None

        return None

    def _update_language_selector(self):
        """Mettre à jour le sélecteur de langue dans le panneau configuration."""
        if not self.language_combo:
            return
        self.language_code_map = localization.get_available_languages()
        self.language_combo.configure(values=list(self.language_code_map.values()))
        fallback_label = localization.t("language_labels.fr")
        current_label = self.language_code_map.get(
            localization.current_language, next(iter(self.language_code_map.values()), fallback_label)
        )
        self.language_combo.set(current_label)

    def _on_language_change(self, _event=None):
        """Gérer la sélection de langue utilisateur."""
        if not self.language_combo:
            return
        selected = self.language_combo.get()
        for code, label in self.language_code_map.items():
            if label == selected and localization.current_language != code:
                localization.set_language(code)
                self.refresh_localized_content()
                break

    def refresh_localized_content(self):
        """Actualiser les libellés dépendants de la langue."""
        try:
            self.root.title(localization.t("app_title"))
        except Exception:
            self.root.title("Web Sentinel")

        for widget, key in self._localized_widgets:
            if isinstance(widget, tk.StringVar):
                widget.set(self._text(key))
            else:
                try:
                    widget.configure(text=self._text(key))
                except tk.TclError:
                    pass

        if hasattr(self, "notebook"):
            try:
                self.notebook.tab(self.domain_tab, text=self._text("tab_domain_scan"))
                self.notebook.tab(self.sast_tab, text=self._text("tab_sast_analysis"))
                self.notebook.tab(self.reports_tab, text=self._text("tab_reports_history"))
                self.notebook.tab(self.config_tab, text=self._text("tab_settings"))
            except Exception:
                pass

        if hasattr(self, "history_tree"):
            self.history_tree.heading("type", text=self._text("history_column_type"))
            self.history_tree.heading("target", text=self._text("history_column_target"))
            self.history_tree.heading("summary", text=self._text("history_column_summary"))
            self.history_tree.heading("timestamp", text=self._text("history_column_date"))
            # Rafraîchir le contenu pour refléter les nouvelles traductions
            self.refresh_history_tab()

        self._update_language_selector()
        self._update_sast_info()
        self.refresh_configuration_tab()
        if self.sast_results:
            self._render_sast_stats(self.sast_results[-1])
        self._update_connection_status()

        if hasattr(self, "status_var") and self._status_key:
            self._set_status(self._status_key, **self._status_kwargs)

    def _text(self, key: str, **kwargs) -> str:
        try:
            return localization.t(key, **kwargs)
        except Exception:
            template = key
        if kwargs:
            try:
                return template.format(**kwargs)
            except (KeyError, ValueError):
                return template
        return template

    def _register_widget(self, widget: Any, key: str) -> None:
        self._localized_widgets.append((widget, key))
        if isinstance(widget, tk.StringVar):
            widget.set(self._text(key))
        else:
            try:
                widget.configure(text=self._text(key))
            except tk.TclError:
                pass

    def _set_status(self, key: str, **kwargs) -> None:
        self._status_key = key
        self._status_kwargs = dict(kwargs)
        if hasattr(self, "status_var"):
            self.status_var.set(self._text(key, **kwargs))
    
    # Méthodes d'interface (stubs pour l'instant)
    def add_domain_to_list(self):
        """Ajouter un domaine à la liste."""
        domain = self.domain_var.get().strip()
        if domain and domain not in self.domain_listbox.get(0, tk.END):
            self.domain_listbox.insert(tk.END, domain)
            self.domain_var.set("")
    
    def remove_selected_domain(self):
        """Supprimer le domaine sélectionné."""
        selection = self.domain_listbox.curselection()
        if selection:
            self.domain_listbox.delete(selection[0])
    
    def clear_domain_list(self):
        """Vider la liste des domaines."""
        self.domain_listbox.delete(0, tk.END)
    
    def browse_folder(self):
        """Parcourir et sélectionner un dossier."""
        self.sast_section.browse_folder()
    
    def browse_files(self):
        """Parcourir et sélectionner des fichiers."""
        self.sast_section.browse_files()
    
    def populate_files_from_folder(self, folder):
        """Peupler la liste avec les fichiers du dossier."""
        self.sast_section.populate_files_from_folder(folder)
    
    def remove_selected_file(self):
        """Supprimer le fichier sélectionné."""
        self.sast_section.remove_selected_file()
    
    def clear_files_list(self):
        """Vider la liste des fichiers."""
        self.sast_section.clear_files_list()
    
    def start_domain_scan(self):
        """Démarrer le scan de domaine."""
        if self.is_scanning:
            messagebox.showinfo(self._text("domain_scan_running_title"), self._text("domain_scan_running_body"))
            return

        domains = [domain.strip() for domain in self.domain_listbox.get(0, tk.END) if domain.strip()]
        if not domains:
            messagebox.showwarning(localization.t("attention_title"), self._text("domain_missing_body"))
            return

        try:
            domain_limit = self.subscription_manager.get_domain_limit()
        except Exception:
            domain_limit = -1

        if domain_limit != -1 and len(domains) > domain_limit:
            messagebox.showwarning(
                self._text("domain_limit_title"),
                self._text("domain_limit_body", limit=domain_limit),
            )
            return

        allow_invasive = self.invasive_var.get()
        can_use_invasive = False
        try:
            can_use_invasive = self.subscription_manager.can_use_invasive_tests()
        except Exception:
            can_use_invasive = False

        if allow_invasive and not can_use_invasive:
            messagebox.showwarning(
                localization.t("attention_title"),
                self._text("domain_invasive_warning"),
            )
            self.invasive_var.set(False)
            allow_invasive = False

        if allow_invasive and not self.invasive_confirmed:
            if not show_invasive_warning(self.root):
                self.invasive_var.set(False)
                allow_invasive = False
            else:
                self.invasive_confirmed = True

        self.save_config()
        self.clear_results()

        self._set_status("status_scan_running")
        self.results_text.insert(tk.END, f"{self._text('scan_started_line', count=len(domains))}\n\n")
        self.results_text.see(tk.END)
        self.progress_var.set(0)
        self.scan_button.config(state=tk.DISABLED)
        self.is_scanning = True

        self.current_scan_thread = threading.Thread(
            target=self._run_domain_scan_thread,
            args=(domains, allow_invasive),
            daemon=True,
        )
        self.current_scan_thread.start()

    def _run_domain_scan_thread(self, domains: List[str], allow_invasive: bool) -> None:
        """Thread worker pour exécuter les scans domaine par domaine."""
        scanner = SentinelScanner()
        enabled_modules = [name for name in scanner.modules if name != "source-code"]
        total = len(domains)

        try:
            for index, domain in enumerate(domains, start=1):
                request = ScanRequest(
                    domain=domain,
                    allow_invasive=allow_invasive,
                )
                
                # Mesurer durée du scan
                import time
                scan_start = time.time()
                result = scanner.run(request, enabled_modules=enabled_modules)
                scan_duration = time.time() - scan_start
                
                # 📊 Télémétrie anonyme (optionnelle, désactivable)
                try:
                    from ..telemetry import send_scan_telemetry
                    from collections import Counter
                    
                    severity_counts = dict(Counter(f.severity for f in result.findings))
                    success = send_scan_telemetry(
                        findings_count=len(result.findings),
                        scan_duration=scan_duration,
                        modules_used=enabled_modules or list(scanner.modules.keys()),
                        severity_counts=severity_counts
                    )
                    if success:
                        print(f"✅ Télémétrie envoyée: {len(result.findings)} findings")
                    else:
                        print(f"⚠️ Télémétrie non envoyée (timeout ou désactivée)")
                except Exception as exc:
                    print(f"❌ Erreur télémétrie: {exc}")  # Debug
                
                self.root.after(0, self._handle_domain_result, result, index, total)
            self.root.after(0, self._domain_scan_completed)
        except Exception as exc:
            self.root.after(0, self._domain_scan_failed, str(exc))

    def _handle_domain_result(self, result: ScanResult, index: int, total: int) -> None:
        """Mettre à jour l'interface après chaque domaine scanné."""
        self.scan_results.append(result)
        self.history_entries.append(result)
        if len(self.history_entries) > 50:
            self.history_entries = self.history_entries[-50:]

        if self.history_var.get():
            try:
                self.history_store.record(result)
            except Exception as exc:  # pragma: no cover - logging only
                LOGGER.warning(self._text("history_save_failed", error=exc))

        summary = self._format_result_summary(result)
        self.results_text.insert(tk.END, summary + "\n\n")
        self.results_text.see(tk.END)

        progress = int((index / total) * 100)
        self.progress_var.set(progress)
        self._set_status("status_scan_progress", index=index, total=total, domain=result.request.domain)
        self.refresh_history_tab()

    def _domain_scan_completed(self) -> None:
        """Appelée lorsque tous les domaines ont été scannés."""
        self.is_scanning = False
        self.current_scan_thread = None
        self.scan_button.config(state=tk.NORMAL)
        self.progress_var.set(100)
        self._set_status("status_scan_done")
        self.refresh_history_tab()

    def _domain_scan_failed(self, error_message: str) -> None:
        """Gérer une erreur lors du scan."""
        self.is_scanning = False
        self.current_scan_thread = None
        self.scan_button.config(state=tk.NORMAL)
        self.progress_var.set(0)
        self._set_status("status_scan_error")
        self.results_text.insert(tk.END, f"❌ {self._text('domain_scan_error', error=error_message)}\n")
        self.results_text.see(tk.END)
        messagebox.showerror(localization.t("error_title"), self._text("domain_scan_error", error=error_message))

    def _format_result_summary(self, result: ScanResult, *, include_details: bool = False) -> str:
        """Retourner une représentation textuelle d'un résultat de scan."""
        domain = result.request.domain
        counts = Counter(finding.severity for finding in result.findings)
        summary_text = self._render_severity_overview(counts)
        header = self._text("domain_result_header", domain=domain, summary=summary_text)
        if not result.findings:
            return header + "\n" + self._text("domain_no_findings")

        if not include_details:
            findings_lines = [
                self._text(
                    "domain_result_line",
                    severity=localization.translate_severity(finding.severity),
                    title=finding.title,
                    module=localization.translate_module(finding.check),
                )
                for finding in result.findings
            ]
            return header + "\n" + "\n".join(findings_lines)

        detailed_lines = []
        for finding in result.findings:
            file_path, line_no = self._extract_finding_location(finding)
            module_name = localization.translate_module(finding.check)
            detailed_lines.append(
                self._text(
                    "domain_result_detail_line",
                    severity=localization.translate_severity(finding.severity),
                    title=finding.title,
                    module=module_name,
                )
            )
            if file_path:
                location = file_path
                if line_no:
                    location = f"{location}:{line_no}"
                detailed_lines.append(self._text("domain_result_file", location=location))
            if finding.description:
                detailed_lines.append(self._text("domain_result_description", description=finding.description))
            if finding.remediation:
                detailed_lines.append(self._text("domain_result_remediation", remediation=finding.remediation))
            code_snippet = self._extract_code_snippet(finding)
            if code_snippet:
                detailed_lines.append(self._text("domain_result_code", code=code_snippet))
        return header + "\n" + "\n".join(detailed_lines)

    def _extract_finding_location(self, finding: Finding) -> tuple[Optional[str], Optional[int]]:
        """Extraire le fichier et la ligne associés à une détection."""
        file_path = None
        line_no: Optional[int] = None

        if finding.i18n_params:
            file_path = finding.i18n_params.get("file") or finding.i18n_params.get("path")
            raw_line = finding.i18n_params.get("line")
            if isinstance(raw_line, int):
                line_no = raw_line
            elif isinstance(raw_line, str) and raw_line.isdigit():
                line_no = int(raw_line)

        evidence = finding.evidence or ""
        if evidence:
            for part in evidence.splitlines():
                lower = part.lower()
                if "file:" in lower and not file_path:
                    candidate = part.split(":", 1)[1].strip()
                    if candidate:
                        file_path = candidate
                if "line:" in lower and line_no is None:
                    line_candidate = part.split(":", 1)[1].strip()
                    if line_candidate.isdigit():
                        line_no = int(line_candidate)

        if file_path:
            try:
                path_obj = Path(file_path)
                if not path_obj.is_absolute() and self._last_sast_root:
                    path_obj = (self._last_sast_root / path_obj).resolve()
                if self._last_sast_root:
                    relative = path_obj.relative_to(self._last_sast_root)
                    file_path = str(relative).replace(os.sep, "/")
                else:
                    file_path = str(path_obj)
            except Exception:
                file_path = str(file_path)

        return file_path, line_no

    def _extract_code_snippet(self, finding: Finding) -> Optional[str]:
        """Récupérer un extrait de code pertinent depuis l'évidence si disponible."""
        evidence = finding.evidence or ""
        if not evidence:
            return None
        for part in evidence.splitlines():
            if part.lower().startswith("code:"):
                snippet = part.split(":", 1)[1].strip()
                if snippet:
                    return snippet[:160] + ("…" if len(snippet) > 160 else "")
        return None

    def _status_symbol(self, condition: bool) -> str:
        """Retourner une coche verte ou rouge selon un booléen."""
        return "✅" if condition else "❌"

    def _format_limit_value(self, value: Optional[int], *, unlimited_key: str) -> str:
        """Formater une limite numérique avec gestion de l'illimité."""
        if value in (-1, None):
            return self._text(unlimited_key)
        return str(value)

    def _format_size_value(self, value: Optional[int]) -> str:
        """Formater une limite de taille en respectant la langue courante."""
        if value in (-1, None):
            return self._text("unlimited_size")
        unit = self._text("unit_mb")
        return f"{value} {unit}"

    def _render_severity_overview(self, counts: Counter) -> str:
        parts = []
        for severity in ("critical", "high", "medium", "low", "info"):
            count = counts.get(severity, 0)
            if not count:
                continue
            label = localization.translate_severity(severity)
            parts.append(f"{label}: {count}")
        return " | ".join(parts) if parts else self._text("no_vulnerabilities")

    def _load_existing_history(self, limit: int = 20) -> None:
        """Précharger quelques résultats d'historique depuis le disque."""
        try:
            raw_history = self.history_store._load()  # type: ignore[attr-defined]
        except Exception as exc:  # pragma: no cover - best effort preload
            LOGGER.debug("Impossible de charger l'historique existant: %s", exc)
            return

        entries: List[ScanResult] = []
        for runs in raw_history.values():
            for payload in runs[-limit:]:
                try:
                    entries.append(ScanResult.from_dict(payload))
                except Exception as exc:
                    LOGGER.debug("Entrée d'historique invalide ignorée: %s", exc)

        entries.sort(key=lambda result: result.generated_at)
        if entries:
            self.history_entries.extend(entries[-limit:])

    def refresh_history_tab(self) -> None:
        """Actualiser la table d'historique."""
        self.history_section.refresh()

    def _on_history_select(self, _event) -> None:
        self.history_section.on_select()

    def _format_history_detail(self, result: ScanResult) -> str:
        scan_type = self._determine_scan_type(result)
        target = self._history_target_label(result)
        timestamp = result.generated_at.astimezone().strftime("%Y-%m-%d %H:%M:%S")
        counts = Counter(finding.severity for finding in result.findings)
        severity_overview = self._render_severity_overview(counts)

        lines = [
            self._text("history_detail_type", value=scan_type),
            self._text("history_detail_target", value=target),
            self._text("history_detail_date", value=timestamp),
            self._text("history_detail_summary", value=severity_overview),
            "",
        ]

        if not result.findings:
            lines.append(self._text("history_detail_no_issue"))
        else:
            for finding in result.findings:
                severity_label = localization.translate_severity(finding.severity)
                lines.append(self._text("history_detail_item", severity=severity_label, title=finding.title))
                lines.append(self._text("history_detail_module", value=localization.translate_module(finding.check)))
                if finding.description:
                    lines.append(self._text("history_detail_description", value=finding.description))
                if finding.remediation:
                    lines.append(self._text("history_detail_remediation", value=finding.remediation))
                if finding.evidence:
                    lines.append(self._text("history_detail_evidence", value=finding.evidence))
                lines.append("")

        return "\n".join(lines).strip()

    def _determine_scan_type(self, result: ScanResult) -> str:
        if result.request.source_path or result.request.source_files:
            return self._text("history_type_sast")
        return self._text("history_type_domain")

    def _history_target_label(self, result: ScanResult) -> str:
        if result.request.source_path:
            return str(result.request.source_path)
        if result.request.source_files:
            files = list(result.request.source_files)
            preview = ", ".join(files[:2])
            return preview if len(files) <= 2 else self._text(
                "history_more_files", preview=preview, extra=len(files) - 2
            )
        return result.request.domain
    
    def start_sast_scan(self):
        """Démarrer l'analyse SAST."""
        self.sast_section.start_scan()

    def _build_sast_config(self, source_path: Optional[Path], raw_files: List[str]) -> tuple[SourceScanConfig, List[Path]]:
        """Prépare une configuration SAST et estime le nombre de fichiers cibles."""
        return self.sast_section._build_config(source_path, raw_files)

    def _run_sast_scan_thread(self, request: ScanRequest) -> None:
        """Exécuter l'analyse SAST dans un thread séparé."""
        self.sast_section._run_scan_thread(request)

    def _handle_sast_result(self, result: ScanResult) -> None:
        """Afficher les résultats SAST."""
        self.sast_section._handle_result(result)

    def _render_sast_stats(self, result: ScanResult) -> None:
        """Mettre à jour les statistiques SAST."""
        self.sast_section._render_stats(result)

    def _sast_scan_failed(self, error_message: str) -> None:
        """Gérer les erreurs d'analyse SAST."""
        self.sast_section._scan_failed(error_message)

    # Méthodes d'export et autres utilitaires
    def export_json(self):
        """Exporter les résultats en JSON."""
        if not self.scan_results:
            messagebox.showinfo(self._text("info_title"), self._text("export_no_results"))
            return

        filename = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[(self._text("json_files"), "*.json"), (self._text("all_files"), "*.*")],
            title=self._text("dialog_export_results_json")
        )
        if not filename:
            return

        try:
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(self.scan_results, f, indent=2, ensure_ascii=False)
            messagebox.showinfo(self._text("export_success_title"), self._text("export_results_success", filename=filename))
        except Exception as exc:
            messagebox.showerror(localization.t("error_title"), self._text("export_results_error", error=exc))

    def export_html(self):
        """Exporter le dernier résultat (domaine ou SAST) en HTML."""
        if not self.scan_results and not self.sast_results:
            messagebox.showinfo(self._text("info_title"), self._text("export_no_report"))
            return

        filename = filedialog.asksaveasfilename(
            defaultextension=".html",
            filetypes=[(self._text("html_files"), "*.html"), (self._text("all_files"), "*.*")],
            title=self._text("dialog_export_results_html")
        )
        if not filename:
            return

        try:
            if self.sast_results:
                export_result = self.sast_results[-1]
            else:
                export_result = self.scan_results[-1]

            context = self.report_engine.build_context(export_result)
            html_payload = self.report_engine.render_html(context)
            Path(filename).write_text(html_payload, encoding="utf-8")
            messagebox.showinfo(self._text("export_success_title"), self._text("export_results_success", filename=filename))
        except Exception as exc:
            messagebox.showerror(localization.t("error_title"), self._text("export_results_error", error=exc))

    def export_sast_json(self):
        """Exporter les résultats SAST en JSON."""
        self.sast_section.export_json()

    def export_sast_html(self):
        """Exporter les résultats SAST en HTML."""
        self.sast_section.export_html()

    def copy_sast_results(self):
        """Copier les résultats SAST dans le presse-papier."""
        self.sast_section.copy_results()

    def _generate_ai_analysis_format(self, scan_result):
        """Génère un format JSON optimisé pour l'analyse par un Agent IA."""
        from datetime import datetime
        
        context = self.report_engine.build_context(scan_result)
        
        # Filtrer seulement les erreurs critiques, élevées et moyennes
        actionable_findings = []
        for finding in context.get("findings", []):
            if finding.get("severity") in ["critical", "high", "medium"]:
                actionable_findings.append({
                    "id": f"{finding.get('check', 'unknown')}_{hash(finding.get('title', ''))}",
                    "module": finding.get("check", "unknown"),
                    "title": finding.get("title", ""),
                    "severity": finding.get("severity", "unknown"),
                    "description": finding.get("description", ""),
                    "remediation": finding.get("remediation", ""),
                    "evidence": finding.get("evidence", ""),
                    "impact": finding.get("impact", ""),
                    "category": self._categorize_finding(finding.get("check", "")),
                    "fix_priority": self._calculate_fix_priority(finding.get("severity", "low"))
                })
        
        # Structure optimisée pour l'Agent IA
        ai_analysis = {
            "metadata": {
                "format_version": "1.0",
                "generated_at": datetime.now().isoformat(),
                "generator": "Web Sentinel Security Scanner",
                "target": {
                    "domain": context.get("request", {}).get("domain", "unknown"),
                    "scan_type": "web_security_audit",
                    "ports_scanned": {
                        "http": context.get("request", {}).get("http_port", 80),
                        "https": context.get("request", {}).get("https_port", 443)
                    }
                },
                "language": localization.current_language
            },
            "analysis_summary": {
                "total_issues": len(actionable_findings),
                "severity_breakdown": {
                    "critical": len([f for f in actionable_findings if f["severity"] == "critical"]),
                    "high": len([f for f in actionable_findings if f["severity"] == "high"]),
                    "medium": len([f for f in actionable_findings if f["severity"] == "medium"])
                },
                "categories_affected": list(set([f["category"] for f in actionable_findings])),
                "requires_immediate_action": len([f for f in actionable_findings if f["severity"] in ["critical", "high"]]) > 0
            },
            "ai_workflow": {
                "step_1": {
                    "action": "analyze_findings",
                    "description": "Analyser chaque finding pour comprendre l'impact et la complexité de correction",
                    "expected_output": "Liste priorisée des vulnérabilités avec estimation de difficulté"
                },
                "step_2": {
                    "action": "confirm_analysis",
                    "description": "Confirmer la validité de chaque finding et éliminer les faux positifs",
                    "expected_output": "Liste validée des vulnérabilités réelles à corriger"
                },
                "step_3": {
                    "action": "create_todolist",
                    "description": "Élaborer une todolist détaillée avec ordre de priorité et dépendances",
                    "expected_output": "Plan d'action structuré avec étapes spécifiques"
                },
                "step_4": {
                    "action": "implement_fixes",
                    "description": "Appliquer les corrections nécessaires selon le plan établi",
                    "expected_output": "Code/configuration corrigé avec documentation des changements"
                }
            },
            "findings": actionable_findings,
            "remediation_context": {
                "common_tools": ["nginx", "apache", "cloudflare", "aws", "docker"],
                "frameworks_detected": self._detect_frameworks(context),
                "server_info": self._extract_server_info(context),
                "security_headers_status": self._analyze_security_headers(actionable_findings)
            },
            "ai_instructions": {
                "primary_goal": "Corriger automatiquement toutes les vulnérabilités de sécurité détectées",
                "approach": "Prioriser par criticité, puis par facilité d'implémentation",
                "constraints": [
                    "Ne pas casser la fonctionnalité existante",
                    "Privilégier les solutions standard de l'industrie",
                    "Documenter chaque changement effectué",
                    "Tester les corrections avant finalisation"
                ],
                "success_criteria": [
                    "Toutes les vulnérabilités critiques/élevées sont corrigées",
                    "Les en-têtes de sécurité sont correctement configurés",
                    "La configuration TLS/SSL est sécurisée",
                    "Aucune régression fonctionnelle introduite"
                ]
            }
        }
        
        return ai_analysis

    def _categorize_finding(self, check_module):
        """Catégorise un finding selon son module."""
        categories = {
            "headers": "security_headers",
            "tls": "ssl_tls_security", 
            "injection": "injection_vulnerabilities",
            "access-control": "access_control",
            "crypto-failures": "cryptographic_failures",
            "insecure-design": "insecure_design",
            "security-misconfiguration": "security_misconfiguration",
            "vulnerable-components": "vulnerable_components",
            "broken-authentication": "authentication_failures",
            "static-analysis": "code_security",
            "osint": "information_disclosure",
            "third-party": "third_party_security"
        }
        return categories.get(check_module, "general_security")

    def _calculate_fix_priority(self, severity):
        """Calcule la priorité de correction."""
        priority_map = {
            "critical": 1,
            "high": 2, 
            "medium": 3,
            "low": 4,
            "info": 5
        }
        return priority_map.get(severity, 5)

    def _detect_frameworks(self, context):
        """Détecte les frameworks utilisés basé sur les findings."""
        frameworks = []
        findings = context.get("findings", [])
        
        for finding in findings:
            evidence = finding.get("evidence", "").lower()
            title = finding.get("title", "").lower()
            description = finding.get("description", "").lower()
            
            text_to_analyze = f"{evidence} {title} {description}"
            
            if "react" in text_to_analyze:
                frameworks.append("React")
            if "angular" in text_to_analyze:
                frameworks.append("Angular")
            if "vue" in text_to_analyze:
                frameworks.append("Vue.js")
            if "django" in text_to_analyze:
                frameworks.append("Django")
            if "flask" in text_to_analyze:
                frameworks.append("Flask")
            if "express" in text_to_analyze or "node" in text_to_analyze:
                frameworks.append("Node.js/Express")
            if "wordpress" in text_to_analyze:
                frameworks.append("WordPress")
        
        return list(set(frameworks))

    def _extract_server_info(self, context):
        """Extrait les informations serveur des findings."""
        server_info = {
            "web_server": "unknown",
            "technologies": [],
            "cms": None
        }
        
        findings = context.get("findings", [])
        for finding in findings:
            evidence = finding.get("evidence", "").lower()
            
            if "nginx" in evidence:
                server_info["web_server"] = "nginx"
            elif "apache" in evidence:
                server_info["web_server"] = "apache"
            elif "iis" in evidence:
                server_info["web_server"] = "iis"
            elif "cloudflare" in evidence:
                server_info["technologies"].append("cloudflare")
                
        return server_info

    def _analyze_security_headers(self, findings):
        """Analyse l'état des en-têtes de sécurité."""
        headers_status = {
            "x_frame_options": False,
            "content_security_policy": False,
            "strict_transport_security": False,
            "x_content_type_options": False,
            "x_xss_protection": False,
            "referrer_policy": False
        }
        
        for finding in findings:
            if finding.get("module") == "headers":
                title = finding.get("title", "").lower()
                if "x-frame-options" in title:
                    headers_status["x_frame_options"] = "missing" in title
                elif "content-security-policy" in title or "csp" in title:
                    headers_status["content_security_policy"] = "missing" in title
                elif "strict-transport-security" in title or "hsts" in title:
                    headers_status["strict_transport_security"] = "missing" in title
                elif "x-content-type-options" in title:
                    headers_status["x_content_type_options"] = "missing" in title
                elif "x-xss-protection" in title:
                    headers_status["x_xss_protection"] = "missing" in title
                elif "referrer-policy" in title:
                    headers_status["referrer_policy"] = "missing" in title
        
        return headers_status

    def export_ai_analysis(self):
        """Exporter les résultats pour analyse par Agent IA (scan domaine)."""
        if not self.scan_results:
            messagebox.showinfo(self._text("info_title"), self._text("export_no_report"))
            return

        # Vérifier qu'il y a des erreurs à corriger
        scan_result = self.scan_results[-1]
        context = self.report_engine.build_context(scan_result)
        actionable_findings = [f for f in context.get("findings", []) 
                              if f.get("severity") in ["critical", "high", "medium"]]
        
        if not actionable_findings:
            messagebox.showinfo(self._text("info_title"), self._text("export_no_issues_for_ai"))
            return

        filename = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[(self._text("json_files"), "*.json"), (self._text("all_files"), "*.*")],
            title=self._text("dialog_export_ai")
        )
        if not filename:
            return

        try:
            ai_analysis = self._generate_ai_analysis_format(scan_result)
            
            import json
            json_payload = json.dumps(ai_analysis, indent=2, ensure_ascii=False)
            Path(filename).write_text(json_payload, encoding="utf-8")
            
            messagebox.showinfo(
                self._text("export_success_title"), 
                self._text("export_ai_success", filename=filename)
            )
        except Exception as exc:
            messagebox.showerror(
                localization.t("error_title"), 
                self._text("export_ai_error", error=exc)
            )

    def export_sast_ai_analysis(self):
        """Exporter les résultats SAST pour analyse par Agent IA."""
        self.sast_section.export_ai()

    def clear_results(self):
        """Effacer les résultats du scan de domaine."""
        self.results_text.delete("1.0", tk.END)
        self.scan_results = []
        self.progress_var.set(0)
    
    def clear_sast_results(self):
        """Effacer les résultats SAST."""
        self.sast_section.clear_results()
    
    def show_license_status(self):
        """Afficher le statut de la licence."""
        try:
            features = self._get_runtime_features()
            tier_attr = getattr(features, "tier", self._resolve_subscription_tier())
            tier_label = tier_attr.value.upper() if isinstance(tier_attr, SubscriptionTier) else str(tier_attr).upper()

            domain_limit = self._format_limit_value(getattr(features, "domain_limit", -1), unlimited_key="unlimited_domains")
            sast_limit = self._format_limit_value(getattr(features, "max_source_files", -1), unlimited_key="unlimited_files")
            size_limit = self._format_size_value(getattr(features, "max_source_size_mb", -1))

            lines = [
                self._text("license_dialog_header", tier=tier_label),
                "",
                self._text("license_dialog_features"),
                self._text("license_feature_domains", status=self._status_symbol(True)),
                self._text("license_feature_invasive", status=self._status_symbol(getattr(features, "allow_invasive_tests", False))),
                self._text("license_feature_html", status=self._status_symbol(getattr(features, "allow_html_export", False))),
                self._text("license_feature_api", status=self._status_symbol(getattr(features, "allow_api_access", False))),
                self._text("license_feature_sast", status=self._status_symbol(getattr(features, "allow_source_scan", False))),
                self._text("license_feature_advanced", status=self._status_symbol(getattr(features, "advanced_rules", False))),
                "",
                self._text("license_dialog_limits"),
                self._text("license_limit_domains", value=domain_limit),
                self._text("license_limit_files", value=sast_limit),
                self._text("license_limit_size", value=size_limit),
            ]

            messagebox.showinfo(self._text("license_dialog_window_title"), "\n".join(lines))
        except Exception as exc:
            title = localization.t("error_title") if hasattr(localization, "t") else "Erreur"
            messagebox.showerror(title, self._text("license_dialog_error", error=exc))
    
    def show_login(self):
        """Afficher la fenêtre de connexion."""
        try:
            login_dialog = LoginDialog(self.root, self.auth_manager)
            result = login_dialog.show(modal=True)
            if result:
                self._synchronize_license_with_auth()
                self.refresh_subscription_ui(self.license_status)
                self._update_connection_status()
                messagebox.showinfo(self._text("login_success_title"), self._text("login_success_message"))
        except Exception as e:
            messagebox.showerror(localization.t("error_title"), self._text("login_error_message", error=e))
    
    def logout(self):
        """Déconnecter l'utilisateur."""
        try:
            self.auth_manager.logout()
            self._synchronize_license_with_auth()
            self.refresh_subscription_ui(self.license_status)
            self._update_connection_status()
            messagebox.showinfo(self._text("info_title"), self._text("logout_success_message"))
        except Exception as e:
            messagebox.showerror(localization.t("error_title"), self._text("logout_error_message", error=e))
    
    def load_config(self):
        """Charger la configuration depuis le fichier."""
        try:
            if self.config_file.exists():
                with open(self.config_file, 'r', encoding='utf-8') as f:
                    self.config = json.load(f)
            else:
                self.config = {
                    'domains': [],
                    'default_options': {
                        'invasive': False,
                        'history': True,
                        'recursive': True,
                        'detailed': False
                    }
                }
        except Exception as e:
            print(f"Erreur lors du chargement de la configuration : {e}")
            self.config = {'domains': [], 'default_options': {}}
    
    def save_config(self):
        """Sauvegarder la configuration dans le fichier."""
        try:
            # Créer le dossier si nécessaire
            self.config_file.parent.mkdir(parents=True, exist_ok=True)
            
            # Mettre à jour la configuration
            self.config['domains'] = list(self.domain_listbox.get(0, tk.END))
            self.config['default_options'] = {
                'invasive': self.invasive_var.get(),
                'history': self.history_var.get(),
                'recursive': self.sast_recursive_var.get(),
                'detailed': self.sast_detailed_var.get()
            }
            
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(self.config, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"Erreur lors de la sauvegarde de la configuration : {e}")
    
    def load_domains_from_config(self):
        """Charger les domaines depuis la configuration."""
        try:
            domains = self.config.get('domains', [])
            for domain in domains:
                self.domain_listbox.insert(tk.END, domain)
            
            # Charger les options par défaut
            options = self.config.get('default_options', {})
            self.invasive_var.set(options.get('invasive', False))
            self.history_var.set(options.get('history', True))
            self.sast_recursive_var.set(options.get('recursive', True))
            self.sast_detailed_var.set(options.get('detailed', False))
            self.refresh_configuration_tab()
        except Exception as e:
            print(f"Erreur lors du chargement des domaines : {e}")
    
    def refresh_subscription_ui(self, license_status: Optional[dict] = None):
        """Rafraîchir les éléments de l'interface dépendants de la licence."""
        try:
            if license_status is not None:
                self.license_status = license_status
            else:
                self._refresh_license_state()

            tier_label = "FREE"
            if getattr(self.subscription_manager, "tier", None):
                tier = self.subscription_manager.tier
                tier_label = tier.upper() if isinstance(tier, str) else tier.value.upper()
            if hasattr(self, "status_var"):
                self._set_status("status_ready_license", tier=tier_label)

            self._update_connection_status()
            self.update_tab_permissions()
            self._update_sast_info()
            self._create_modern_user_section()
            self.refresh_configuration_tab()
        except Exception as e:
            print(f"Erreur lors du rafraîchissement de l'interface : {e}")
    
    def on_recursive_changed(self):
        """Gestionnaire du changement d'option récursive."""
        # Re-scanner le dossier si un dossier est sélectionné
        folder = self.folder_var.get()
        if folder and os.path.exists(folder):
            self.populate_files_from_folder(folder)
    
    def on_closing(self):
        """Gestionnaire de fermeture de l'application."""
        self.save_config()
        self.root.quit()
        self.root.destroy()
    
    def change_theme(self):
        """Permet de changer de thème instantanément (aligné sur le GUI classique)."""
        def _on_theme_change(theme_name: str):
            self._apply_theme(theme_name, persist=True)
            messagebox.showinfo(
                "Thème appliqué",
                f"Le thème '{theme_name}' est actif immédiatement.",
                parent=self.root,
            )
        
        show_theme_selector(self.root, self.current_theme, _on_theme_change)
    
    def run(self):
        """Démarrer l'interface graphique."""
        try:
            self.root.mainloop()
        except KeyboardInterrupt:
            self.root.quit()


def main():
    """Point d'entrée principal."""
    app = ModernWebSentinelGUI()
    app.run()


if __name__ == "__main__":
    main()
