"""
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 types import SimpleNamespace
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 (
    SourceCodeScanner,
    SourceScanConfig,
    DEFAULT_EXCLUSIONS,
    LANGUAGE_EXTENSIONS,
)
from ..reporting import HistoryStore, ReportEngine
from ..subscription import SubscriptionManager, SubscriptionTier
from ..subscription.validator import LicenseError as SubscriptionLicenseError, LicenseValidator
from ..payment.payment_modal import PaymentModalController
from ..payment.stripe_client import StripeClient
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 ..subscription.models import get_features_for_tier
from .i18n import localization
from .invasive_warning import show_invasive_warning
from .license_dialog import LicenceOperationError, show_license_status_dialog

LOGGER = logging.getLogger(__name__)


class ModernWebSentinelGUI:
    """Interface Web Sentinel moderne avec design amélioré et onglets fonctionnels."""
    
    def __init__(self, subscription_tier: str | SubscriptionTier | 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')
        
        # Style moderne
        self.setup_modern_style()
        
        # 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._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()
        
        # Gestion de la fermeture
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
    
    def setup_modern_style(self):
        """Configuration du style moderne."""
        style = ttk.Style()
        
        # Configuration du thème de base
        style.theme_use('clam')
        
        # Couleurs modernes
        colors = {
            'primary': '#2563eb',      # Bleu moderne
            'secondary': '#64748b',    # Gris bleuté
            'success': '#059669',      # Vert
            'warning': '#d97706',      # Orange
            'danger': '#dc2626',       # Rouge
            'background': '#f8fafc',   # Fond clair
            'surface': '#ffffff',      # Surface blanche
            'border': '#e2e8f0'       # Bordure légère
        }
        
        # 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', '#1d4ed8'),
                            ('pressed', '#1e40af')])
        
        # 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='#1f2937',
                       font=('Segoe UI', 12, 'bold'))
        
        style.configure('Subtitle.TLabel',
                       background=colors['surface'],
                       foreground=colors['secondary'],
                       font=('Segoe UI', 10))
    
    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))
                
                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")
                print(f"⚠️ Image non trouvée: {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)
                
                print(f"🔄 Image redimensionnée: {new_width}×{new_height} (frame: {frame_width}×{frame_height})")
                
        except Exception as e:
            print(f"❌ Erreur lors du redimensionnement: {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.reports_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.reports_tab, text=self._text("tab_reports_history"))
        self.setup_modern_reports_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 setup_modern_reports_tab(self):
        """Configuration de l'onglet rapports avec design moderne."""
        main_frame = ttk.Frame(self.reports_tab, style='Modern.TFrame', padding=20)
        main_frame.pack(fill=tk.BOTH, expand=True)

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

        table_frame = ttk.Frame(main_frame, style='Modern.TFrame')
        table_frame.pack(fill=tk.BOTH, expand=True)
        table_frame.columnconfigure(0, weight=1)
        table_frame.rowconfigure(0, weight=1)

        columns = ("type", "target", "summary", "timestamp")
        self.history_tree = ttk.Treeview(table_frame, columns=columns, show='headings', height=12)
        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"))

        self.history_tree.column("type", width=90, stretch=False)
        self.history_tree.column("target", width=220)
        self.history_tree.column("summary", width=360)
        self.history_tree.column("timestamp", width=160, stretch=False)

        tree_scroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=tree_scroll.set)

        self.history_tree.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        tree_scroll.grid(row=0, column=1, sticky=(tk.N, tk.S))

        self.history_detail_frame = ttk.LabelFrame(main_frame, padding=15)
        self.history_detail_frame.pack(fill=tk.BOTH, expand=True, pady=(20, 0))
        self.history_detail_frame.columnconfigure(0, weight=1)
        self.history_detail_frame.rowconfigure(0, weight=1)
        self._register_widget(self.history_detail_frame, "history_detail_frame")

        self.history_detail_text = scrolledtext.ScrolledText(
            self.history_detail_frame,
            wrap=tk.WORD,
            font=('Consolas', 10),
            height=10,
            state=tk.DISABLED,
        )
        self.history_detail_text.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))

        self.history_tree.bind("<<TreeviewSelect>>", self._on_history_select)
        self.refresh_history_tab()
    
    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)
        
        # 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."""
        user = self.auth_manager.get_current_user()
        subscription_manager = self.auth_manager.get_subscription_manager()
        owner_email = getattr(user, "email", None) if user else None
        self.license_initialized = self.license_bridge.apply_subscription(
            subscription_manager.tier, owner_email
        )
        self._refresh_license_state()

    def _refresh_license_state(self):
        """Récupérer les objets runtime dépendants de la licence."""
        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()

    def _resolve_subscription_tier(self) -> SubscriptionTier:
        """Determine the active subscription tier with graceful fallbacks."""
        tier = getattr(self.subscription_manager, "tier", None)
        if isinstance(tier, SubscriptionTier):
            return tier
        if isinstance(tier, str):
            try:
                return SubscriptionTier(tier.lower())
            except ValueError:
                pass
        status = getattr(self, "license_status", {}) or {}
        tier_label = status.get("tier")
        if isinstance(tier_label, str):
            try:
                return SubscriptionTier(tier_label.lower())
            except ValueError:
                pass
        return SubscriptionTier.FREE

    def _get_runtime_features(self):
        """Return a mutable view of the current feature set."""
        # Vérifier d'abord si l'utilisateur est SYSOP
        user = self.auth_manager.get_current_user()
        user_role = str(getattr(user, "role", "")).upper() if user else ""
        
        # SYSOP a toujours toutes les fonctionnalités activées
        # Vérifier si le rôle contient "SYSOP" (pour gérer USERROLE.SYSOP et SYSOP)
        if "SYSOP" in user_role:
            return SimpleNamespace(
                tier=SubscriptionTier.SYSOP,
                domain_limit=-1,
                max_users=-1,
                allow_invasive_tests=True,
                allow_html_export=True,
                allow_multi_user=True,
                allow_api_access=True,
                allow_source_scan=True,
                max_source_files=-1,
                max_source_size_mb=-1,
                advanced_rules=True
            )
        
        # Pour les autres utilisateurs, récupérer les features normalement
        features_obj = None
        if self.subscription_manager:
            try:
                features_obj = self.subscription_manager.get_features()
            except Exception as exc:
                LOGGER.debug("Impossible d'obtenir les fonctionnalités via SubscriptionManager: %s", exc)
        if features_obj is None:
            base_features = get_features_for_tier(self._resolve_subscription_tier())
            features_obj = SimpleNamespace(**base_features.__dict__)
        elif not isinstance(features_obj, SimpleNamespace):
            features_obj = SimpleNamespace(**features_obj.__dict__)

        status = getattr(self, "license_status", {}) or {}
        features_enabled = status.get("features_enabled", {})
        if isinstance(features_enabled, dict):
            if "source_scan" in features_enabled:
                features_obj.allow_source_scan = bool(features_enabled["source_scan"])
            if "advanced_rules" in features_enabled:
                features_obj.advanced_rules = bool(features_enabled["advanced_rules"])
        if "max_source_files" in status and status["max_source_files"] is not None:
            features_obj.max_source_files = status["max_source_files"]
        if "max_source_size_mb" in status and status["max_source_size_mb"] is not None:
            features_obj.max_source_size_mb = status["max_source_size_mb"]

        return features_obj
    
    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."""
        try:
            features = self._get_runtime_features()
            if getattr(features, "allow_source_scan", False):
                return True
            status = getattr(self, "license_status", {}) or {}
            tier_label = str(status.get("tier", "")).upper()
            if tier_label in {"PRO", "ENTERPRISE", "SYSOP"}:
                return True
            user = self.auth_manager.get_current_user()
            if user and str(getattr(user, "role", "")).upper() == "SYSOP":
                return True
            if self.license_validator:
                return self.license_validator.can_use_feature('source_scan')
        except Exception as e:
            LOGGER.warning(self._text("sast_check_error", error=e))
        return False
    
    def _update_sast_info(self):
        """Mettre à jour les informations SAST selon les permissions."""
        for widget in self.sast_info_frame.winfo_children():
            widget.destroy()
        
        if self._can_use_sast():
            features = self._get_runtime_features()
            max_files_text = self._format_limit_value(getattr(features, 'max_source_files', -1), unlimited_key='unlimited_files')
            max_size_text = self._format_size_value(getattr(features, 'max_source_size_mb', -1))
            info_text = self._text("sast_info_limits", files=max_files_text, size=max_size_text)
            color = '#059669'  # Vert
        else:
            info_text = self._text("sast_info_disabled")
            color = '#dc2626'  # Rouge

        info_label = ttk.Label(self.sast_info_frame, text=info_text, 
                              foreground=color, font=('Segoe UI', 10, 'bold'))
        info_label.pack()
        if hasattr(self, "sast_scan_button"):
            state = tk.NORMAL if self._can_use_sast() else tk.DISABLED
            self.sast_scan_button.config(state=state)

    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."""
        folder = filedialog.askdirectory(title=self._text("dialog_select_folder"))
        if folder:
            self.folder_var.set(folder)
            self.populate_files_from_folder(folder)
    
    def browse_files(self):
        """Parcourir et sélectionner des fichiers."""
        files = filedialog.askopenfilenames(
            title=self._text("dialog_select_files"),
            filetypes=[
                (self._text("source_files_filter"), "*.php *.js *.py *.java *.cs *.go *.cpp *.c *.h"),
                ("Tous les fichiers", "*.*")
            ]
        )
        if files:
            for file in files:
                if file not in self.files_listbox.get(0, tk.END):
                    self.files_listbox.insert(tk.END, file)
    
    def populate_files_from_folder(self, folder):
        """Peupler la liste avec les fichiers du dossier."""
        # Clear existing files
        self.files_listbox.delete(0, tk.END)
        
        # Extensions supportées
        extensions = {'.php', '.js', '.py', '.java', '.cs', '.go', '.cpp', '.c', '.h'}
        
        folder_path = Path(folder)
        if self.sast_recursive_var.get():
            pattern = "**/*"
        else:
            pattern = "*"
        
        for file_path in folder_path.glob(pattern):
            if file_path.is_file() and file_path.suffix.lower() in extensions:
                self.files_listbox.insert(tk.END, str(file_path))
    
    def remove_selected_file(self):
        """Supprimer le fichier sélectionné."""
        selection = self.files_listbox.curselection()
        if selection:
            self.files_listbox.delete(selection[0])
    
    def clear_files_list(self):
        """Vider la liste des fichiers."""
        self.files_listbox.delete(0, tk.END)
    
    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,
                )
                result = scanner.run(request, enabled_modules=enabled_modules)
                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."""
        if not hasattr(self, "history_tree"):
            return

        for item in self.history_tree.get_children():
            self.history_tree.delete(item)
        self.history_tree_data.clear()

        for result in reversed(self.history_entries):
            counts = Counter(finding.severity for finding in result.findings)
            summary = self._render_severity_overview(counts)
            scan_type = self._determine_scan_type(result)
            target = self._history_target_label(result)
            timestamp = result.generated_at.astimezone().strftime("%Y-%m-%d %H:%M")
            item_id = self.history_tree.insert("", "end", values=(scan_type, target, summary, timestamp))
            self.history_tree_data[item_id] = result

        if hasattr(self, "history_detail_text"):
            self.history_detail_text.config(state=tk.NORMAL)
            self.history_detail_text.delete("1.0", tk.END)
            if self.history_entries:
                self.history_detail_text.insert(tk.END, self._text("history_no_entries_prompt"))
            else:
                self.history_detail_text.insert(tk.END, self._text("history_no_entries"))
            self.history_detail_text.config(state=tk.DISABLED)

    def _on_history_select(self, _event) -> None:
        if not hasattr(self, "history_tree"):
            return
        selection = self.history_tree.selection()
        if not selection:
            return
        item_id = selection[0]
        result = self.history_tree_data.get(item_id)
        if not result or not hasattr(self, "history_detail_text"):
            return

        detail_text = self._format_history_detail(result)
        self.history_detail_text.config(state=tk.NORMAL)
        self.history_detail_text.delete("1.0", tk.END)
        self.history_detail_text.insert(tk.END, detail_text)
        self.history_detail_text.config(state=tk.DISABLED)

    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."""
        if not self._can_use_sast():
            messagebox.showerror(self._text("error_access_denied_title"), self._text("error_access_denied_body"))
            return

        if self.current_sast_thread and self.current_sast_thread.is_alive():
            messagebox.showinfo(self._text("info_sast_running_title"), self._text("info_sast_running_body"))
            return

        self._expected_sast_files = None

        folder = self.folder_var.get().strip()
        files = [path for path in self.files_listbox.get(0, tk.END) if path]

        if not folder and not files:
            messagebox.showwarning(localization.t("attention_title"), self._text("sast_scan_incomplete_body"))
            return

        source_path = None
        if folder:
            source_path = Path(folder).expanduser().resolve()
            if not source_path.exists():
                messagebox.showerror(localization.t("error_title"), self._text("error_folder_missing", path=source_path))
                return
            if not source_path.is_dir():
                messagebox.showerror(localization.t("error_title"), self._text("error_folder_invalid", path=source_path))
                return

        preview_config, candidate_paths = self._build_sast_config(source_path, files)
        if not candidate_paths:
            warning_message = self._text("sast_no_files_warning")
            self.sast_results_text.insert(tk.END, warning_message + "\n\n")
            self.sast_results_text.see(tk.END)
            self._set_status("status_no_source")
            messagebox.showwarning(localization.t("attention_title"), warning_message)
            return
        self._expected_sast_files = len(candidate_paths)

        features = self._get_runtime_features()
        languages_tuple = tuple(sorted(preview_config.languages))
        exclude_tuple = tuple(sorted(preview_config.exclude_patterns - set(DEFAULT_EXCLUSIONS)))

        request = ScanRequest(
            domain=f"SAST:{source_path.name if source_path else 'Sélection'}",
            source_path=str(source_path) if source_path else None,
            source_files=tuple(str(path) for path in candidate_paths) if not source_path else (),
            source_languages=languages_tuple,
            source_exclude=exclude_tuple,
            source_recursive=preview_config.recursive,
            source_detailed_report=preview_config.detailed_report,
            source_max_files=getattr(features, 'max_source_files', -1),
            source_max_size_mb=getattr(features, 'max_source_size_mb', -1),
            source_advanced_rules=getattr(features, 'advanced_rules', False),
        )

        self.save_config()
        self.clear_sast_results()
        folder_display = str(source_path) if source_path else self._text("manual_selection_label")
        info_line = (
            f"{self._text('sast_selected_folder', path=folder_display)}\n"
            f"{self._text('sast_detected_files', count=len(candidate_paths))}\n"
        )
        self.sast_results_text.insert(tk.END, info_line)
        self.sast_results_text.see(tk.END)
        self.sast_results_text.insert(tk.END, f"{self._text('sast_scan_started_line', count=len(candidate_paths))}\n\n")
        self.sast_results_text.see(tk.END)
        self._set_status("status_sast_running")
        self.sast_scan_button.config(state=tk.DISABLED)
        self.sast_progress_var.set(10)
        self.current_sast_thread = threading.Thread(
            target=self._run_sast_scan_thread,
            args=(request,),
            daemon=True,
        )
        self.current_sast_thread.start()

    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."""
        resolved_files = [Path(path).expanduser().resolve() for path in raw_files]
        languages = {"auto"}
        exclude_patterns = set(DEFAULT_EXCLUSIONS)
        recursive = bool(self.sast_recursive_var.get())
        detailed = bool(self.sast_detailed_var.get())
        features = self._get_runtime_features()

        config = SourceScanConfig(
            source_path=source_path,
            source_files=resolved_files,
            languages=languages,
            exclude_patterns=exclude_patterns,
            min_severity="medium",
            max_files=getattr(features, "max_source_files", -1),
            max_size_mb=getattr(features, "max_source_size_mb", -1),
            advanced_rules=getattr(features, "advanced_rules", False),
            recursive=recursive,
            detailed_report=detailed,
        )

        preview_scanner = SourceCodeScanner(config)
        candidate_files = preview_scanner._discover_files()

        detected_languages = set()
        for file_path in candidate_files:
            ext = file_path.suffix.lower()
            for lang, extensions in LANGUAGE_EXTENSIONS.items():
                if ext in extensions:
                    detected_languages.add(lang)
                    break
        if detected_languages:
            config.languages = detected_languages

        if source_path:
            self._last_sast_root = source_path
        elif candidate_files:
            common = os.path.commonpath([str(path.parent) for path in candidate_files])
            self._last_sast_root = Path(common)
        else:
            self._last_sast_root = None

        return config, candidate_files

    def _run_sast_scan_thread(self, request: ScanRequest) -> None:
        """Exécuter l'analyse SAST dans un thread séparé."""
        scanner = SentinelScanner()
        try:
            result = scanner.run(request, enabled_modules=["source-code"])
            self.root.after(0, self._handle_sast_result, result)
        except Exception as exc:
            self.root.after(0, self._sast_scan_failed, str(exc))

    def _handle_sast_result(self, result: ScanResult) -> None:
        """Afficher les résultats SAST."""
        self.sast_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_sast_save_failed", error=exc))

        summary = self._format_result_summary(result, include_details=True)
        if self._expected_sast_files is not None:
            summary = f"{summary}\n{self._text('sast_files_analyzed_line', count=self._expected_sast_files)}"
        self.sast_results_text.insert(tk.END, summary + "\n\n")
        self.sast_results_text.see(tk.END)

        self._render_sast_stats(result)
        self.refresh_history_tab()

        self.sast_progress_var.set(100)
        self.sast_scan_button.config(state=tk.NORMAL)
        self._set_status("status_sast_done")
        self.current_sast_thread = None
        self._expected_sast_files = None

    def _render_sast_stats(self, result: ScanResult) -> None:
        """Mettre à jour les statistiques SAST."""
        for widget in self.sast_stats_frame.winfo_children():
            widget.destroy()

        counts = Counter(finding.severity for finding in result.findings)
        if not counts:
            ttk.Label(
                self.sast_stats_frame,
                text=self._text("summary_no_issue"),
                foreground="#059669",
                font=('Segoe UI', 10, 'bold'),
            ).pack(anchor=tk.W)
        else:
            color_map = {
                "critical": "#dc2626",
                "high": "#f97316",
                "medium": "#facc15",
                "low": "#0ea5e9",
                "info": "#64748b",
            }

            for severity in ("critical", "high", "medium", "low", "info"):
                count = counts.get(severity, 0)
                if count:
                    severity_label = localization.translate_severity(severity)
                    ttk.Label(
                        self.sast_stats_frame,
                        text=f"{severity_label}: {count}",
                        foreground=color_map.get(severity, "#1f2937"),
                        font=('Segoe UI', 10, 'bold'),
                    ).pack(anchor=tk.W)

        languages = getattr(result.request, "source_languages", ()) or ("auto",)
        lang_set = sorted({lang for lang in languages if lang})
        lang_text = ", ".join(lang_set)
        if not lang_text:
            lang_text = self._text("language_auto")
        ttk.Label(
            self.sast_stats_frame,
            text=self._text("sast_languages_label", languages=lang_text),
            foreground="#334155",
            font=('Segoe UI', 9, 'italic'),
        ).pack(anchor=tk.W, pady=(8, 0))

    def _sast_scan_failed(self, error_message: str) -> None:
        """Gérer les erreurs d'analyse SAST."""
        self.sast_progress_var.set(0)
        self.sast_scan_button.config(state=tk.NORMAL)
        self._set_status("status_sast_error")
        self.sast_results_text.insert(tk.END, f"❌ {self._text('error_sast_failed', error=error_message)}\n")
        self.sast_results_text.see(tk.END)
        self.current_sast_thread = None
        self._expected_sast_files = None
        messagebox.showerror(localization.t("error_title"), self._text("error_sast_failed", error=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."""
        if not self.sast_results:
            messagebox.showinfo(self._text("info_title"), self._text("no_sast_results"))
            return

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

        try:
            export_result = self.sast_results[-1]
            context = self.report_engine.build_context(export_result)
            json_payload = self.report_engine.render_json(context)
            Path(filename).write_text(json_payload, encoding="utf-8")
            messagebox.showinfo(self._text("export_success_title"), self._text("export_sast_json_message", filename=filename))
        except Exception as exc:
            messagebox.showerror(localization.t("error_title"), self._text("export_sast_json_error", error=exc))

    def export_sast_html(self):
        """Exporter les résultats SAST en HTML."""
        if not self.sast_results:
            messagebox.showinfo(self._text("info_title"), self._text("no_sast_results"))
            return

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

        try:
            export_result = self.sast_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_sast_html_message", filename=filename))
        except Exception as exc:
            messagebox.showerror(localization.t("error_title"), self._text("export_sast_html_error", error=exc))

    def copy_sast_results(self):
        """Copier les résultats SAST dans le presse-papier."""
        content = self.sast_results_text.get("1.0", tk.END)
        self.root.clipboard_clear()
        self.root.clipboard_append(content)
        messagebox.showinfo(self._text("info_title"), self._text("export_sast_copy_message"))

    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."""
        if not self.sast_results:
            messagebox.showinfo(self._text("info_title"), self._text("no_sast_results"))
            return

        # Vérifier qu'il y a des erreurs à corriger
        sast_result = self.sast_results[-1]
        context = self.report_engine.build_context(sast_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(sast_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 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_results_text.delete("1.0", tk.END)
        self.sast_progress_var.set(0)
        self.sast_results = []
        self._last_sast_root = None
        # Effacer les statistiques
        for widget in self.sast_stats_frame.winfo_children():
            widget.destroy()
    
    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 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()
