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

from ..model import ScanRequest
from ..scanner import SentinelScanner
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 .i18n import localization
from .invasive_warning import show_invasive_warning
from .license_dialog import LicenceOperationError, show_license_status_dialog
from .themes import ThemeManager
from .theme_selector import show_theme_selector


class WebSentinelGUI:
    def __init__(self, subscription_tier: str | SubscriptionTier | None = None):
        self.root = tk.Tk()
        self.root.title(localization.t("app_title"))
        self.root.geometry("1200x800")
        self.root.minsize(1000, 600)
        
        # Configuration du redimensionnement automatique
        self.root.grid_rowconfigure(0, weight=1)
        self.root.grid_columnconfigure(0, weight=1)
        
        # Variables
        self.domains_var = tk.StringVar()
        self.allow_invasive_var = tk.BooleanVar()
        self.invasive_confirmed = False  # Pour suivre si l'utilisateur a confirmé les tests invasifs
        self.timeout_var = tk.DoubleVar(value=5.0)
        self.current_scan_thread = None
        self.scan_results = []
        self.is_scanning = False
        
        # Variables des modules (initialisées ici pour éviter les erreurs)
        self.module_vars = {
            # Modules existants
            'tls': tk.BooleanVar(value=True),
            'headers': tk.BooleanVar(value=True),
            'static-analysis': tk.BooleanVar(value=True),
            'injection': tk.BooleanVar(value=True),
            'third-party': tk.BooleanVar(value=False),
            'source-code': tk.BooleanVar(value=False),
            
            # Nouveaux modules OWASP Top 10
            'access-control': tk.BooleanVar(value=True),
            'crypto-failures': tk.BooleanVar(value=True),
            'vulnerable-components': tk.BooleanVar(value=True),
            'insecure-design': tk.BooleanVar(value=True),
            'security-misconfiguration': tk.BooleanVar(value=True),
            'broken-authentication': tk.BooleanVar(value=True)
        }
        
        # Variables SAST
        self.source_path_var = tk.StringVar()
        self.source_languages_var = tk.StringVar(value="php,javascript,python,java,csharp,go")
        self.source_exclude_var = tk.StringVar(value="node_modules/*,vendor/*,.git/*")

        # Système d'authentification
        self.auth_manager = AuthManager()
        
        # 🔌 PRIORITÉ 1 : Intégration système licence embarquée
        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._license_refresh_job: Optional[str] = None

        stripe_test_mode = os.getenv("STRIPE_LIVE_MODE", "").lower() not in {"1", "true", "yes"}
        api_env_var = "STRIPE_TEST_KEY" if stripe_test_mode else "STRIPE_LIVE_KEY"
        api_key = os.getenv(api_env_var)
        if not api_key and stripe_test_mode:
            # Fallback pour développement - ne jamais utiliser en production
            api_key = None  # Force l'utilisation des variables d'environnement
        self.payment_controller = PaymentModalController(
            StripeClient(test_mode=stripe_test_mode, api_key=api_key)
        )

        # Configuration
        self.config_file = Path.home() / ".web-sentinel" / "gui-config.json"
        self.load_config()
        
        # Style et thème (initialisation avant setup_ui)
        self.style = ttk.Style()
        self.current_theme = self.config.get("theme", "default")
        
        self.setup_ui()
        
        # Application du thème APRÈS la création de l'UI
        ThemeManager.apply_theme(self.root, self.style, self.current_theme)
        
        self.load_domains_from_config()
        self.refresh_subscription_ui(self.license_status)
        self._schedule_license_refresh()
        
        # Gestion du redimensionnement
        self.root.bind('<Configure>', self.on_window_configure)
        
    def on_window_configure(self, event):
        """Gérer le redimensionnement de la fenêtre."""
        if event.widget == self.root:
            # Ajuster la hauteur de la zone de résultats si nécessaire
            if hasattr(self, 'results_frame'):
                self.adjust_results_height()
    
    def adjust_results_height(self):
        """Ajuster automatiquement la hauteur de la zone de résultats."""
        if hasattr(self, 'results_tree') and self.scan_results:
            # Calculer la hauteur nécessaire pour afficher tous les résultats
            num_rows = len(self.scan_results)
            if num_rows > 0:
                # Hauteur minimum : 5 lignes, maximum : 15 lignes
                optimal_height = max(5, min(15, num_rows + 1))
                self.results_tree.configure(height=optimal_height)
        
    def setup_ui(self):
        """Créer l'interface utilisateur principale avec support multilingue."""
        # Le style est déjà configuré dans __init__ avec le thème
        
        # Frame principal avec gestion du redimensionnement
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Configuration des poids de grille pour l'auto-redimensionnement
        main_frame.columnconfigure(0, weight=1)
        main_frame.rowconfigure(4, weight=1)  # Section résultats extensible
        
        # Header avec titre et sélecteur de langue
        header_frame = ttk.Frame(main_frame)
        header_frame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 20))
        header_frame.columnconfigure(0, weight=1)
        
        # Titre à gauche
        self.title_label = ttk.Label(header_frame, text=localization.t("app_title"), 
                               font=("Arial", 16, "bold"))
        self.title_label.grid(row=0, column=0, sticky="w")
        
        # Zone centrale (compte utilisateur)
        self.user_frame = ttk.Frame(header_frame)
        self.user_frame.grid(row=0, column=1, padx=(10, 10))
        self._create_user_section()
        
        # Sélecteur de langue à droite
        lang_frame = ttk.Frame(header_frame)
        lang_frame.grid(row=0, column=2, sticky="e")
        
        ttk.Label(lang_frame, text=localization.t("language_label")).grid(row=0, column=0, padx=(0, 5))
        self.language_combo = ttk.Combobox(lang_frame, values=list(localization.get_available_languages().values()), 
                                          state="readonly", width=15)
        self.language_combo.grid(row=0, column=1)
        self.language_combo.set(localization.get_available_languages()[localization.current_language])
        self.language_combo.bind('<<ComboboxSelected>>', self.on_language_change)
        
        # Bouton sélecteur de thème
        theme_btn = ttk.Button(lang_frame, 
                              text="🎨",
                              width=3,
                              command=self.open_theme_selector)
        theme_btn.grid(row=0, column=2, padx=(10, 0))
        
        # Configuration des sections
        self.create_domain_section(main_frame)
        self.create_options_section(main_frame) 
        self.create_control_section(main_frame)
        self.create_results_section(main_frame)
        self.create_status_bar(main_frame)
    
    def _create_user_section(self):
        """Créer la section utilisateur dans l'en-tête."""
        # Effacer le contenu existant
        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:
                # Utilisateur connecté
                user_info = ttk.Label(
                    self.user_frame,
                    text=f"👤 {user.display_name}",
                    font=("Arial", 10)
                )
                user_info.grid(row=0, column=0, padx=(0, 10))
                
                # Tier d'abonnement
                tier_text = self.subscription_manager.tier.value.upper()
                tier_colors = {
                    "FREE": "#6b7280",
                    "PRO": "#2563eb", 
                    "ENTERPRISE": "#7c3aed"
                }
                tier_label = ttk.Label(
                    self.user_frame,
                    text=f"🎯 {tier_text}",
                    font=("Arial", 9, "bold"),
                    foreground=tier_colors.get(tier_text, "#6b7280")
                )
                tier_label.grid(row=0, column=1, padx=(0, 10))
                
                # Bouton déconnexion
                logout_btn = ttk.Button(
                    self.user_frame,
                    text="Déconnexion",
                    command=self._on_logout,
                    width=12
                )
                logout_btn.grid(row=0, column=2)
        else:
            # Utilisateur non connecté
            guest_label = ttk.Label(
                self.user_frame,
                text="👤 Mode invité (FREE)",
                font=("Arial", 10),
                foreground="#6b7280"
            )
            guest_label.grid(row=0, column=0, padx=(0, 10))
            
            # Bouton connexion
            login_btn = ttk.Button(
                self.user_frame,
                text="Se connecter",
                command=self._on_login,
                width=12
            )
            login_btn.grid(row=0, column=1)
    
    def _on_login(self):
        """Ouvrir la fenêtre de connexion."""
        login_dialog = LoginDialog(self.root, self.auth_manager)
        login_dialog.on_success = self._on_login_success
        login_dialog.show(modal=True)
    
    def _on_login_success(self):
        """Traiter une connexion réussie."""
        self._synchronize_license_with_auth()
        # Rafraîchir l'interface utilisateur
        self.subscription_manager = self.license_bridge.get_runtime_manager()
        self.license_validator = self.license_bridge.get_runtime_validator()
        self._create_user_section()
        self.refresh_subscription_ui(self.license_bridge.get_license_status())

        # Message de bienvenue
        user = self.auth_manager.get_current_user()
        if user:
            messagebox.showinfo(
                "Connexion réussie",
                f"Bienvenue {user.display_name} !\nTier: {self.subscription_manager.tier.value.upper()}"
            )

    def _on_logout(self):
        """Déconnecter l'utilisateur."""
        if messagebox.askyesno("Déconnexion", "Voulez-vous vraiment vous déconnecter ?"):
            self.auth_manager.logout()
            self._synchronize_license_with_auth()
            self.subscription_manager = self.license_bridge.get_runtime_manager()
            self.license_validator = self.license_bridge.get_runtime_validator()
            self._create_user_section()
            self.refresh_subscription_ui(self.license_bridge.get_license_status())
            messagebox.showinfo("Déconnexion", localization.t("license_status_free_mode"))

    def _synchronize_license_with_auth(self) -> None:
        """Mettre en phase la licence embarquée avec la session courante."""
        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
        )
    
    def on_language_change(self, event=None):
        """Changer la langue de l'interface."""
        selected = self.language_combo.get()
        for code, name in localization.get_available_languages().items():
            if name == selected:
                localization.set_language(code)
                self.refresh_ui_text()
                break
    
    def open_theme_selector(self):
        """Ouvrir le sélecteur de thème."""
        def on_theme_change(theme_name: str):
            """Callback appelé quand un nouveau thème est sélectionné."""
            self.current_theme = theme_name
            ThemeManager.apply_theme(self.root, self.style, theme_name)
            
            # Sauvegarder le thème dans la configuration
            self.config["theme"] = theme_name
            self.save_config()
            
            # Rafraîchir les tags du treeview avec les nouvelles couleurs
            self.refresh_treeview_tags()
            
            messagebox.showinfo(
                "Thème appliqué",
                f"Le thème '{theme_name}' a été appliqué avec succès !\n\n"
                "Certains éléments seront mieux affichés au prochain démarrage."
            )
        
        show_theme_selector(self.root, self.current_theme, on_theme_change)
    
    def refresh_treeview_tags(self):
        """Rafraîchit les tags du treeview avec les couleurs du thème actuel."""
        theme = ThemeManager.get_current_theme()
        if theme and hasattr(self, 'results_tree'):
            colors = theme.colors
            self.results_tree.tag_configure("critical", 
                                           background=colors.critical_bg, 
                                           foreground=colors.critical_fg)
            self.results_tree.tag_configure("high", 
                                           background=colors.high_bg, 
                                           foreground=colors.high_fg)
            self.results_tree.tag_configure("medium", 
                                           background=colors.medium_bg, 
                                           foreground=colors.medium_fg)
            self.results_tree.tag_configure("low", 
                                           background=colors.low_bg, 
                                           foreground=colors.low_fg)
            self.results_tree.tag_configure("info", 
                                           background=colors.info_severity_bg, 
                                           foreground=colors.info_severity_fg)
    
    def refresh_ui_text(self):
        """Rafraîchir tous les textes de l'interface après changement de langue."""
        self.root.title(localization.t("app_title"))
        
        # Mettre à jour le titre principal
        if hasattr(self, 'title_label'):
            self.title_label.config(text=localization.t("app_title"))
        
        # Mettre à jour les labels des sections
        if hasattr(self, 'domains_frame'):
            self.domains_frame.config(text=localization.t("domains_section"))
        if hasattr(self, 'options_frame'):
            self.options_frame.config(text=localization.t("options_section"))
        if hasattr(self, 'results_frame'):
            self.results_frame.config(text=localization.t("results_section"))
            
        # Mettre à jour les labels de domaines
        if hasattr(self, 'domains_label_widget'):
            self.domains_label_widget.config(text=localization.t("domains_hint").split('\n')[0])
            
        # Mettre à jour les boutons
        if hasattr(self, 'load_btn'):
            self.load_btn.config(text=localization.t("btn_load"))
        if hasattr(self, 'save_btn'):
            self.save_btn.config(text=localization.t("btn_save"))
        if hasattr(self, 'clear_btn'):
            self.clear_btn.config(text=localization.t("btn_clear"))
        if hasattr(self, 'scan_btn'):
            if not self.is_scanning:
                self.scan_btn.config(text=localization.t("btn_start_scan"))
        if hasattr(self, 'stop_button'):
            self.stop_button.config(text=localization.t("btn_stop_scan"))
        if hasattr(self, 'export_btn'):
            self.export_btn.config(text=localization.t("btn_export"))
        if hasattr(self, 'upgrade_button'):
            if self.subscription_manager.tier == SubscriptionTier.FREE:
                self.upgrade_button.config(text=localization.t("subscription_upgrade_cta"))
            else:
                self.upgrade_button.config(text=localization.t("subscription_manage_plan"))
            
        # Mettre à jour les options
        if hasattr(self, 'invasive_check'):
            self.invasive_check.config(text=localization.t("invasive_mode"))
        if hasattr(self, 'timeout_label_widget'):
            self.timeout_label_widget.config(text=localization.t("timeout_label"))
        if hasattr(self, 'modules_label_widget'):
            self.modules_label_widget.config(text=localization.t("modules_label"))
            
        # Mettre à jour les colonnes du tableau
        if hasattr(self, 'results_tree'):
            self.results_tree.heading('#0', text=localization.t("col_domain"))
            self.results_tree.heading('module', text=localization.t("col_module"))
            self.results_tree.heading('severity', text=localization.t("col_severity"))
            self.results_tree.heading('title', text=localization.t("col_title"))
            self.results_tree.heading('status', text=localization.t("col_status"))
            
        # Mettre à jour les résumés et statuts
        if hasattr(self, 'summary_label'):
            if not self.scan_results:
                self.summary_label.config(text=localization.t("no_scan_performed"))
            
        # Mettre à jour le statut
        if hasattr(self, 'status_label'):
            if hasattr(self, 'is_scanning') and self.is_scanning:
                self.status_label.config(text=localization.t("status_scanning"))
            else:
                self._update_status_bar_with_license(self.license_status if hasattr(self, "license_status") else {"status": "FREE_MODE"})

        if hasattr(self, 'license_button'):
            self.license_button.config(text=localization.t("license_button_label_default"))
            if hasattr(self, "license_status"):
                self._update_license_button_appearance(self.license_status)
            
        # Actualiser les résultats avec les nouvelles traductions
        if hasattr(self, 'scan_results') and self.scan_results:
            self.display_results()
        
    def create_domain_section(self, parent):
        """Section de gestion des domaines."""
        # Frame pour les domaines
        self.domains_frame = ttk.LabelFrame(parent, text=localization.t("domains_section"), padding="10")
        self.domains_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
        self.domains_frame.columnconfigure(0, weight=1)
        
        # Zone de texte pour les domaines avec label
        self.domains_label_widget = ttk.Label(self.domains_frame, text=localization.t("domains_hint").split('\n')[0])
        self.domains_label_widget.grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
        
        self.domains_text = scrolledtext.ScrolledText(self.domains_frame, height=6, width=60)
        self.domains_text.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # Insérer le texte d'exemple
        placeholder_text = "exemple.com\nmonsite.fr\napi.monapp.com"
        self.domains_text.insert("1.0", placeholder_text)
        
        # Boutons de gestion des domaines
        button_frame = ttk.Frame(self.domains_frame)
        button_frame.grid(row=2, column=0, sticky=tk.W)
        
        self.load_btn = ttk.Button(button_frame, text=localization.t("btn_load"), 
                  command=self.load_domains_file)
        self.load_btn.pack(side=tk.LEFT, padx=(0, 5))
        
        self.save_btn = ttk.Button(button_frame, text=localization.t("btn_save"), 
                  command=self.save_domains_file)
        self.save_btn.pack(side=tk.LEFT, padx=(0, 5))
        
        self.clear_btn = ttk.Button(button_frame, text=localization.t("btn_clear"), 
                  command=self.clear_domains)
        self.clear_btn.pack(side=tk.LEFT, padx=(0, 5))
        
    def create_options_section(self, parent):
        """Section des options de scan."""
        self.options_frame = ttk.LabelFrame(parent, text=localization.t("options_section"), padding="10")
        self.options_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # Options en ligne
        row1_frame = ttk.Frame(self.options_frame)
        row1_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # Timeout
        self.timeout_label_widget = ttk.Label(row1_frame, text=localization.t("timeout_label"))
        self.timeout_label_widget.grid(row=0, column=0, padx=(0, 5))
        
        timeout_spinbox = ttk.Spinbox(row1_frame, from_=1.0, to=30.0, increment=1.0, 
                                     textvariable=self.timeout_var, width=10)
        timeout_spinbox.grid(row=0, column=1, padx=(0, 20))
        
        # Mode invasif avec callback
        self.invasive_check = ttk.Checkbutton(row1_frame, text=localization.t("invasive_mode"), 
                                     variable=self.allow_invasive_var,
                                     command=self._on_invasive_toggle)
        self.invasive_check.grid(row=0, column=2, sticky=tk.W)
        if not self.subscription_manager.can_use_invasive_tests():
            self.allow_invasive_var.set(False)
            self.invasive_check.state(["disabled"])
        
        # Modules à exécuter
        row2_frame = ttk.Frame(self.options_frame)
        row2_frame.grid(row=1, column=0, sticky=(tk.W, tk.E))
        
        self.modules_label_widget = ttk.Label(row2_frame, text=localization.t("modules_label"))
        self.modules_label_widget.grid(row=0, column=0, padx=(0, 10), sticky=tk.W)
        
        # Checkboxes des modules (variables déjà initialisées dans __init__)
        col = 1
        for module, var in self.module_vars.items():
            translated_name = localization.translate_module(module)
            cb = ttk.Checkbutton(row2_frame, text=translated_name, variable=var, 
                               command=lambda m=module: self._on_module_toggle(m))
            cb.grid(row=0, column=col, padx=(0, 10), sticky=tk.W)
            
            # Désactiver source-code si licence insuffisante
            if module == 'source-code' and not self._can_use_sast():
                cb.state(["disabled"])
                var.set(False)
            
            col += 1
        
        # Section SAST (visible seulement si activée)
        self.sast_frame = ttk.LabelFrame(self.options_frame, text="🔍 Analyse de code source (SAST)", padding="10")
        self.sast_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(10, 0))
        self.sast_frame.grid_remove()  # Masqué par défaut
        
        # Sélection de dossier source
        source_path_frame = ttk.Frame(self.sast_frame)
        source_path_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        
        ttk.Label(source_path_frame, text="Dossier source:").grid(row=0, column=0, padx=(0, 5))
        
        self.source_path_entry = ttk.Entry(source_path_frame, textvariable=self.source_path_var, width=40)
        self.source_path_entry.grid(row=0, column=1, padx=(0, 5))
        
        ttk.Button(source_path_frame, text="Parcourir...", 
                  command=self._browse_source_folder).grid(row=0, column=2)
        
        # Options SAST
        sast_options_frame = ttk.Frame(self.sast_frame)
        sast_options_frame.grid(row=1, column=0, sticky=(tk.W, tk.E))
        
        # Langages
        ttk.Label(sast_options_frame, text="Langages:").grid(row=0, column=0, padx=(0, 5), sticky=tk.W)
        self.source_languages_entry = ttk.Entry(sast_options_frame, textvariable=self.source_languages_var, width=30)
        self.source_languages_entry.grid(row=0, column=1, padx=(0, 20))
        
        # Exclusions
        ttk.Label(sast_options_frame, text="Exclure:").grid(row=0, column=2, padx=(0, 5), sticky=tk.W)
        self.source_exclude_entry = ttk.Entry(sast_options_frame, textvariable=self.source_exclude_var, width=30)
        self.source_exclude_entry.grid(row=0, column=3)
            
    def create_control_section(self, parent):
        """Section de contrôle du scan."""
        self.controls_frame = ttk.Frame(parent)
        self.controls_frame.grid(row=3, column=0, columnspan=3, pady=(0, 20))
        
        self.scan_btn = ttk.Button(self.controls_frame, text=localization.t("btn_start_scan"), 
                                     command=self.start_scan, style="Accent.TButton")
        self.scan_btn.pack(side=tk.LEFT, padx=(0, 10))
        
        self.stop_button = ttk.Button(self.controls_frame, text=localization.t("btn_stop_scan"), 
                                     command=self.stop_scan, state=tk.DISABLED)
        self.stop_button.pack(side=tk.LEFT, padx=(0, 10))
        
        self.export_btn = ttk.Button(self.controls_frame, text=localization.t("btn_export"), 
                                     command=self.export_report, state=tk.DISABLED)
        self.export_btn.pack(side=tk.LEFT)
        
    def create_results_section(self, parent):
        """Section d'affichage des résultats."""
        self.results_frame = ttk.LabelFrame(parent, text=localization.t("results_section"), padding="10")
        self.results_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
        self.results_frame.columnconfigure(0, weight=1)
        self.results_frame.rowconfigure(1, weight=1)
        
        # Résumé
        self.summary_label = ttk.Label(self.results_frame, text=localization.t("no_scan_performed"), 
                                      font=("Arial", 10, "bold"))
        self.summary_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 10))
        
        # Tableau des résultats
        table_frame = ttk.Frame(self.results_frame)
        table_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        table_frame.columnconfigure(0, weight=1)
        table_frame.rowconfigure(0, weight=1)
        
        columns = ("module", "severity", "title", "status")
        self.results_tree = ttk.Treeview(table_frame, columns=columns, show="tree headings", height=12)
        
        # Configuration des colonnes avec textes traduits
        self.results_tree.heading("#0", text=localization.t("col_domain"))  # Colonne pour l'arbre = domaine
        self.results_tree.heading("module", text=localization.t("col_module"))
        self.results_tree.heading("severity", text=localization.t("col_severity"))
        self.results_tree.heading("title", text=localization.t("col_title"))
        self.results_tree.heading("status", text=localization.t("col_status"))
        
        self.results_tree.column("#0", width=200, minwidth=150)  # Colonne domaine élargie
        self.results_tree.column("module", width=100)
        self.results_tree.column("severity", width=80)
        self.results_tree.column("title", width=350)
        self.results_tree.column("status", width=100)
        
        # Scrollbars
        v_scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.results_tree.yview)
        h_scrollbar = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.results_tree.xview)
        
        self.results_tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
        
        self.results_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        v_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
        h_scrollbar.grid(row=1, column=0, sticky=(tk.W, tk.E))
        
        # Double-clic pour voir les détails
        self.results_tree.bind("<Double-1>", self.show_finding_details)
        
        # Tags pour les couleurs de sévérité (configurés dynamiquement avec le thème)
        self.refresh_treeview_tags()
        
        # Tag pour les en-têtes de domaine
        theme = ThemeManager.get_current_theme()
        if theme:
            self.results_tree.tag_configure("domain_header", 
                                           background=theme.colors.success, 
                                           foreground="white", 
                                           font=("Arial", 10, "bold"))
        else:
            self.results_tree.tag_configure("domain_header", 
                                           background="#e8f5e8", 
                                           foreground="darkgreen", 
                                           font=("Arial", 10, "bold"))
        
    def create_status_bar(self, parent):
        """Barre de statut."""
        status_frame = ttk.Frame(parent)
        status_frame.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E))
        status_frame.columnconfigure(0, weight=1)
        
        self.status_label = ttk.Label(status_frame, text=localization.t("status_ready"), 
                                    relief=tk.SUNKEN, anchor=tk.W)
        self.status_label.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
        
        self.progress_bar = ttk.Progressbar(status_frame, mode='indeterminate', length=200)
        self.progress_bar.grid(row=0, column=1)

        self.upgrade_button = ttk.Button(
            status_frame,
            text=localization.t("subscription_upgrade_cta"),
            command=lambda: self.show_upgrade_prompt("upgrade"),
            style="Accent.TButton",
        )
        self.upgrade_button.grid(row=0, column=2, padx=(10, 0))
        
        # 🔐 PRIORITÉ 1 : Bouton "Gérer licence" 
        self.license_button = ttk.Button(
            status_frame,
            text=localization.t("license_button_label_default"),
            command=self._show_license_status,
            style="Accent.TButton",
        )
        self.license_button.grid(row=0, column=3, padx=(5, 0))
        
    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": [], "last_settings": {}}
        except Exception:
            self.config = {"domains": [], "last_settings": {}}
            
    def save_config(self):
        """Sauvegarder la configuration."""
        try:
            self.config_file.parent.mkdir(parents=True, exist_ok=True)
            self.config["last_settings"] = {
                "timeout": self.timeout_var.get(),
                "allow_invasive": self.allow_invasive_var.get(),
                "modules": {k: v.get() for k, v in self.module_vars.items()}
            }
            self.config["subscription_tier"] = self.subscription_manager.tier.value
            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:
            messagebox.showerror(localization.t("error_title"), 
                               f"{localization.t('error_save_failed')}: {e}")
            
    def load_domains_from_config(self):
        """Charger les domaines et paramètres depuis la configuration."""
        domains = self.config.get("domains", [])
        if domains and hasattr(self, 'domains_text'):
            self.domains_text.delete(1.0, tk.END)
            self.domains_text.insert(1.0, "\n".join(domains))
        
        # Charger les paramètres précédents
        last_settings = self.config.get("last_settings", {})
        if last_settings:
            if "timeout" in last_settings:
                self.timeout_var.set(last_settings["timeout"])
            if "allow_invasive" in last_settings:
                # Ne pas activer automatiquement, l'utilisateur doit toujours confirmer
                # self.allow_invasive_var.set(last_settings["allow_invasive"])
                pass  # Laissé commenté pour forcer la confirmation à chaque session
            if "modules" in last_settings:
                for module, value in last_settings["modules"].items():
                    if module in self.module_vars:
                        self.module_vars[module].set(value)

        tier_hint = self.config.get("subscription_tier")
        license_tier = self.subscription_manager.tier.value if self.subscription_manager else None
        if tier_hint:
            if self.license_status and self.license_status.get("status") == "LICENSED":
                if license_tier and tier_hint.lower() != license_tier.lower():
                    self.config["subscription_tier"] = license_tier
            else:
                self.subscription_manager.set_tier(tier_hint)
        if hasattr(self, "invasive_check"):
            if not self.subscription_manager.can_use_invasive_tests():
                self.allow_invasive_var.set(False)
                self.invasive_check.state(["disabled"])
            else:
                self.invasive_check.state(["!disabled"])
        
    def load_domains_file(self):
        """Charger les domaines depuis un fichier."""
        filename = filedialog.askopenfilename(
            title=localization.t("load_domains_hint"),
            filetypes=[
                (localization.t("json_files"), "*.json"), 
                (localization.t("text_files"), "*.txt"), 
                (localization.t("all_files"), "*.*")
            ]
        )
        
        if filename:
            try:
                with open(filename, 'r', encoding='utf-8') as f:
                    if filename.endswith('.json'):
                        data = json.load(f)
                        if isinstance(data, list):
                            domains = data
                        else:
                            domains = data.get("domains", [])
                    else:
                        domains = [line.strip() for line in f.readlines() if line.strip()]
                
                self.domains_text.delete(1.0, tk.END)
                self.domains_text.insert(1.0, "\n".join(domains))
                self.status_label.config(text=localization.t("success_domains_loaded", len(domains), Path(filename).name))
                
            except Exception as e:
                messagebox.showerror(localization.t("error_title"), 
                                   localization.t("error_load_failed") + f": {e}")
                
    def save_domains_file(self):
        """Sauvegarder les domaines vers un fichier."""
        filename = filedialog.asksaveasfilename(
            title=localization.t("save_domains_hint"),
            defaultextension=".json",
            filetypes=[
                (localization.t("json_files"), "*.json"), 
                (localization.t("text_files"), "*.txt")
            ]
        )
        
        if filename:
            try:
                domains = self.get_domains()
                with open(filename, 'w', encoding='utf-8') as f:
                    if filename.endswith('.json'):
                        json.dump({"domains": domains, "created": datetime.datetime.now().isoformat()}, 
                                f, indent=2, ensure_ascii=False)
                    else:
                        f.write("\n".join(domains))
                
                self.status_label.config(text=localization.t("success_domains_saved", len(domains), Path(filename).name))
                
            except Exception as e:
                messagebox.showerror(localization.t("error_title"), 
                                   localization.t("error_save_failed") + f": {e}")
                
    def clear_domains(self):
        """Effacer la liste des domaines."""
        if messagebox.askyesno(localization.t("confirm_title"), 
                              localization.t("confirm_clear")):
            self.domains_text.delete(1.0, tk.END)
            self.domains_text.insert(1.0, localization.t("domains_hint"))
    
    def _on_invasive_toggle(self):
        """Gérer le changement du mode invasif avec avertissement."""
        current_state = self.allow_invasive_var.get()
        
        # Si l'utilisateur tente d'activer le mode invasif
        if current_state:
            try:
                self.license_validator.ensure_invasive_tests_allowed()
            except SubscriptionLicenseError:
                self.allow_invasive_var.set(False)
                self.notify_subscription_requirement()
                return
            # Si ce n'est pas encore confirmé, afficher la modal d'avertissement
            if not self.invasive_confirmed:
                if show_invasive_warning(self.root):
                    # L'utilisateur a confirmé
                    self.invasive_confirmed = True
                    self.status_label.config(text="🚨 Mode invasif activé - Tests d'injection autorisés")
                else:
                    # L'utilisateur a annulé, remettre à False
                    self.allow_invasive_var.set(False)
                    self.status_label.config(text=localization.t("status_ready"))
            else:
                # Déjà confirmé précédemment
                self.status_label.config(text="🚨 Mode invasif activé - Tests d'injection autorisés")
        else:
            # L'utilisateur désactive le mode invasif
            self.invasive_confirmed = False
            self.status_label.config(text=localization.t("status_ready"))

    def notify_subscription_requirement(self):
        """Informer l'utilisateur qu'un abonnement supérieur est requis."""
        self.show_upgrade_prompt("invasive")

    def can_start_invasive_scan(self) -> bool:
        """Indiquer si l'utilisateur peut activer les tests invasifs."""
        try:
            self.license_validator.ensure_invasive_tests_allowed()
        except SubscriptionLicenseError:
            return False
        return True

    def refresh_subscription_ui(self, license_status: Optional[dict] = None):
        """
        🔐 PRIORITÉ 1 : Mettre à jour l'état visuel selon l'abonnement et licence.
        
        Prend en compte le système de licence embarquée et alertes d'expiration.
        """
        if license_status is None:
            license_status = self.license_bridge.get_license_status()
        self.license_status = license_status

        can_invasive = self.subscription_manager.can_use_invasive_tests()
        if hasattr(self, "invasive_check"):
            if can_invasive:
                self.invasive_check.state(["!disabled"])
            else:
                self.invasive_check.state(["disabled"])
                self.allow_invasive_var.set(False)

        # Gestion bouton upgrade/manage selon contexte
        if hasattr(self, "upgrade_button"):
            if self.subscription_manager.tier == SubscriptionTier.FREE:
                self.upgrade_button.state(["!disabled"])
                self.upgrade_button.config(text=localization.t("subscription_upgrade_cta"))
            else:
                self.upgrade_button.config(text=localization.t("subscription_manage_plan"))
                
        # 🔐 Gestion bouton licence avec alertes visuelles
        if hasattr(self, "license_button"):
            try:
                self._update_license_button_appearance(license_status)
            except Exception:
                self.license_button.config(text=localization.t("license_button_label_default"), style="Accent.TButton")

        if not self.is_scanning:
            self._update_status_bar_with_license(license_status)
                
    def _update_license_button_appearance(self, license_status: dict) -> None:
        """
        Met à jour l'apparence du bouton licence selon le statut et alertes.
        
        Args:
            license_status: Statut depuis LicenseRuntimeBridge
        """
        status = license_status.get("status", "ERROR")
        alert_level = license_status.get("alert_level")
        
        # Icônes et styles selon statut
        label = localization.t("license_button_label_short")
        if status == "LICENSED":
            if alert_level == "CRITICAL":
                self.license_button.config(text=f"🚨 {label}", style="Accent.TButton")
            elif alert_level == "WARNING":
                self.license_button.config(text=f"⚠️ {label}", style="Accent.TButton")
            elif alert_level == "INFO":
                self.license_button.config(text=f"ℹ️ {label}", style="Accent.TButton")
            else:
                self.license_button.config(text=f"✅ {label}", style="Accent.TButton")
        elif status == "FREE_MODE":
            self.license_button.config(text=f"🆓 {label}", style="Accent.TButton")
        elif status == "ERROR":
            self.license_button.config(text=f"❌ {label}", style="Accent.TButton")
        else:
            self.license_button.config(text=f"🔐 {label}", style="Accent.TButton")

    def _update_status_bar_with_license(self, license_status: dict) -> None:
        """Mettre à jour le message de la barre de statut en fonction de la licence."""
        if not hasattr(self, "status_label"):
            return

        status = license_status.get("status")
        alert_level = license_status.get("alert_level")
        threshold = license_status.get("alert_threshold")

        if status == "LICENSED":
            if not license_status.get("valid", True) or (alert_level == "CRITICAL" and threshold == 0):
                message = localization.t("license_status_expired")
            elif alert_level == "CRITICAL":
                message = localization.t("license_status_critical", threshold)
            elif alert_level == "WARNING":
                message = localization.t("license_status_warning", threshold)
            elif alert_level == "INFO":
                message = localization.t("license_status_info", threshold)
            else:
                message = localization.t("status_ready")
        elif status == "FREE_MODE":
            message = localization.t("license_status_free_mode")
        elif status == "ERROR":
            error_text = license_status.get("error") or ""
            message = localization.t("license_status_error", error_text)
        else:
            message = localization.t("status_ready")

        self.status_label.config(text=message)

    def _schedule_license_refresh(self, delay_ms: int = 6 * 60 * 60 * 1000) -> None:
        """Programmer le prochain rafraîchissement licence."""
        if self._license_refresh_job is not None:
            self.root.after_cancel(self._license_refresh_job)
        self._license_refresh_job = self.root.after(delay_ms, self._handle_license_refresh)

    def _handle_license_refresh(self) -> None:
        """Rafraîchir la licence périodiquement et mettre à jour l'UI."""
        self.license_bridge.refresh()
        status = self.license_bridge.get_license_status()
        self.subscription_manager = self.license_bridge.get_runtime_manager()
        self.license_validator = self.license_bridge.get_runtime_validator()
        self.refresh_subscription_ui(status)
        self._schedule_license_refresh()

    def show_upgrade_prompt(self, feature: str) -> None:
        """Afficher une fenêtre d'incitation à la mise à niveau."""
        messages = {
            "invasive": "subscription_invasive_not_allowed",
            "domains": "subscription_upgrade_message_domains",
            "html_export": "subscription_upgrade_message_html_export",
            "upgrade": "subscription_upgrade_message_generic",
        }
        message_key = messages.get(feature, "subscription_invasive_not_allowed")

        if not self.payment_controller.client.is_configured():
            messagebox.showinfo(
                localization.t("subscription_upgrade_title"),
                localization.t("subscription_payment_unavailable"),
            )
            return

        self.show_upgrade_modal(feature, message_key)

    def show_upgrade_modal(self, feature: str, message_key: str) -> None:
        """Créer et afficher la fenêtre de sélection d'abonnement."""
        dialog = UpgradeDialog(self, localization.t(message_key))
        self.root.wait_window(dialog.window)

    def open_checkout(self, tier: str, cadence: str) -> Optional[str]:
        """Ouvrir une session Stripe de paiement."""
        try:
            url = self.payment_controller.open_checkout(tier, cadence)
        except RuntimeError as exc:
            messagebox.showerror(
                localization.t("error_title"),
                localization.t("subscription_checkout_error", str(exc)),
            )
            return None
        else:
            self.status_label.config(text=localization.t("subscription_checkout_opened"))
            return url

    def _show_license_status(self) -> None:
        """
        🔐 PRIORITÉ 1 : Afficher la fenêtre de statut de licence.
        
        Interface gestion licence avec alertes expiration graduelles.
        """
        status_provider = self.license_bridge.get_license_status

        def import_callback(path: Path) -> bool:
            return self._import_license_from_dialog(path)

        show_license_status_dialog(self.root, status_provider, import_callback)

    def _import_license_from_dialog(self, path: Path) -> bool:
        """Importer une nouvelle licence depuis la fenêtre de gestion."""
        try:
            success = self.license_bridge.install_new_license(path)
        except EmbeddedLicenceError as exc:
            message = localization.t("license_import_error", str(exc))
            raise LicenceOperationError(message) from exc

        status = self.license_bridge.get_license_status()
        self.subscription_manager = self.license_bridge.get_runtime_manager()
        self.license_validator = self.license_bridge.get_runtime_validator()
        self.refresh_subscription_ui(status)
        return success


    def get_domains(self) -> List[str]:
        """Récupérer la liste des domaines."""
        content = self.domains_text.get(1.0, tk.END).strip()
        domains = [line.strip() for line in content.split('\n') if line.strip()]
        return domains

    def get_selected_modules(self) -> List[str]:
        """Récupérer les modules sélectionnés."""
        return [name for name, var in self.module_vars.items() if var.get()]

    def start_scan(self):
        """Lancer le scan en arrière-plan."""
        domains = self.get_domains()
        if not domains:
            messagebox.showwarning(localization.t("attention_title"), 
                                 localization.t("error_no_domains"))
            return

        domain_limit = self.subscription_manager.get_domain_limit()
        if domain_limit != -1 and len(domains) > domain_limit:
            self.show_upgrade_prompt("domains")
            self.status_label.config(text=localization.t("subscription_upgrade_needed"))
            return "upgrade_required"
            
        selected_modules = self.get_selected_modules()
        if not selected_modules:
            messagebox.showwarning(localization.t("attention_title"), 
                                 localization.t("error_no_modules"))
            return
        
        # Validation SAST
        if not self._validate_sast_config():
            return
            
        # Sauvegarder la configuration
        self.config["domains"] = domains
        self.save_config()
        
        # Préparer l'interface
        self.scan_btn.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)
        self.export_btn.config(state=tk.DISABLED)
        self.progress_bar.start()
        self.clear_results()
        self.is_scanning = True
        
        # Mettre à jour le statut
        self.status_label.config(text=localization.t("status_scanning"))
        
        # Lancer le scan en thread séparé
        self.current_scan_thread = threading.Thread(
            target=self.run_scan_thread,
            args=(domains, selected_modules),
            daemon=True
        )
        self.current_scan_thread.start()
        
    def run_scan_thread(self, domains: List[str], modules: List[str]):
        """Exécuter le scan dans un thread séparé."""
        try:
            scanner = SentinelScanner()
            total_domains = len(domains)
            
            for i, domain in enumerate(domains, 1):
                self.root.after(0, lambda d=domain, idx=i, tot=total_domains: 
                               self.status_label.config(text=localization.t("scanning_domain", f"{d} ({idx}/{tot})")))
                
                request = ScanRequest(
                    domain=domain,
                    timeout=int(self.timeout_var.get()),
                    allow_invasive=self.allow_invasive_var.get(),
                    # Paramètres SAST
                    source_path=self.source_path_var.get().strip() if self.module_vars.get('source-code', tk.BooleanVar()).get() else None,
                    source_languages=self.source_languages_var.get().strip() if self.module_vars.get('source-code', tk.BooleanVar()).get() else None,
                    source_exclude=self.source_exclude_var.get().strip() if self.module_vars.get('source-code', tk.BooleanVar()).get() else None
                )
                
                try:
                    result = scanner.run(request, enabled_modules=modules)
                    self.scan_results.append(result)
                    
                    # Mettre à jour l'interface depuis le thread principal
                    self.root.after(0, self.add_scan_result, domain, result)
                    
                except Exception as e:
                    self.root.after(0, self.add_scan_error, domain, str(e))
                    
        except Exception as e:
            self.root.after(0, self.scan_error, str(e))
        else:
            self.root.after(0, self.scan_completed)
            
    def display_results(self):
        """Afficher les résultats de scan avec structure hiérarchique par domaine."""
        # Vider le tableau existant
        for item in self.results_tree.get_children():
            self.results_tree.delete(item)
        
        if not self.scan_results:
            self.summary_label.config(text=localization.t("no_scan_performed"))
            return
            
        # Organiser les résultats par domaine
        results_by_domain = {}
        total_findings = 0
        
        for result in self.scan_results:
            domain = result.request.domain
            if domain not in results_by_domain:
                results_by_domain[domain] = []
            results_by_domain[domain].extend(result.findings)
            total_findings += len(result.findings)
        
        domains_count = len(results_by_domain)
        
        # Créer la structure hiérarchique
        for domain, findings in results_by_domain.items():
            # Calculer les statistiques du domaine
            domain_stats = {}
            for finding in findings:
                severity = finding.severity.lower()
                domain_stats[severity] = domain_stats.get(severity, 0) + 1
            
            # Créer le texte de résumé du domaine
            stats_text = []
            for severity in ["critical", "high", "medium", "low", "info"]:
                count = domain_stats.get(severity, 0)
                if count > 0:
                    sev_translated = localization.translate_severity(severity)
                    stats_text.append(f"{sev_translated}: {count}")
            
            domain_summary = f"{len(findings)} findings" + (f" ({', '.join(stats_text)})" if stats_text else "")
            
            # Insérer le nœud parent (domaine)
            domain_node = self.results_tree.insert("", "end", 
                text=f"🌐 {domain}",
                values=("", "", domain_summary, ""),
                tags=("domain_header",),
                open=True)  # Ouvert par défaut
            
            # Ajouter les findings sous ce domaine
            for finding in findings:
                # Traduire les éléments
                severity_translated = localization.translate_severity(finding.severity)
                module_translated = localization.translate_module(finding.check)
                
                # Insérer le finding sous le domaine
                self.results_tree.insert(domain_node, "end",
                    text="  🔍",
                    values=(
                        module_translated, 
                        severity_translated, 
                        finding.title,
                        localization.t('severity_' + finding.severity.lower())
                    ),
                    tags=(finding.severity.lower(),))
        
        # Mettre à jour le résumé
        self.summary_label.config(
            text=localization.t("scan_summary", domains_count, total_findings)
        )
        
        # Ajuster la hauteur du tableau automatiquement
        if total_findings > 0:
            optimal_height = max(6, min(15, total_findings + 1))
            self.results_tree.configure(height=optimal_height)
    
    def add_scan_result(self, domain: str, result):
        """Ajouter un résultat de scan."""
        self.scan_results.append(result)
        self.display_results()
            
    def add_scan_error(self, domain: str, error: str):
        """Ajouter une erreur de scan."""
        # Créer un résultat d'erreur fictif
        from ..model import ScanResult, Finding
        error_finding = Finding(
            check="error",
            title=localization.t("error_scan_failed") + " " + str(error),
            severity="critical", 
            description=str(error),
            remediation=localization.t("error_scan_failed"),
        )
        error_result = ScanResult(target=domain, findings=[error_finding])
        self.scan_results.append(error_result)
        self.display_results()
        
    def scan_completed(self):
        """Scan terminé avec succès."""
        self.scan_btn.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        self.export_btn.config(state=tk.NORMAL if self.scan_results else tk.DISABLED)
        self.progress_bar.stop()
        
        self.status_label.config(text=localization.t("status_completed"))
        self.is_scanning = False
    
    def show_finding_details(self, event):
        """Afficher les détails d'une observation dans une popup."""
        selection = self.results_tree.selection()
        if not selection:
            return
            
        item = selection[0]
        item_data = self.results_tree.item(item)
        values = item_data["values"]
        text = item_data["text"]
        
        # Vérifier si c'est un en-tête de domaine (qui commence par 🌐)
        if text.startswith("🌐"):
            return  # Ne pas afficher de détails pour les en-têtes de domaine
            
        # C'est un finding individuel - récupérer les données
        if len(values) < 4:
            return
            
        # Nouveau format: (module_translated, severity_translated, title, status)
        module, severity, title, status = values
        
        # Récupérer le domaine depuis le parent
        parent_item = self.results_tree.parent(item)
        if parent_item:
            parent_data = self.results_tree.item(parent_item)
            parent_text = parent_data["text"]
            # Extraire le domaine du texte "🌐 domain.com"
            domain = parent_text.replace("🌐 ", "")
        else:
            domain = localization.t("unknown_domain")
        
        # Trouver l'observation correspondante
        finding = None
        for result in self.scan_results:
            if result.request.domain == domain:
                for f in result.findings:
                    if (f.title == title and 
                        localization.translate_module(f.check) == module and
                        localization.translate_severity(f.severity) == severity):
                        finding = f
                        break
                if finding:
                    break
                    
        if not finding:
            return
        
        # Traduire le contenu du finding selon la langue actuelle
        translated_finding = localization.translate_finding_content(finding)
            
        # Créer la popup de détails
        details_window = tk.Toplevel(self.root)
        details_window.title(localization.t("btn_details"))
        details_window.geometry("600x500")
        details_window.resizable(True, True)
        
        # Frame principal avec scrollbar
        main_frame = ttk.Frame(details_window, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Titre (maintenant traduit)
        title_label = ttk.Label(main_frame, text=translated_finding.title, 
                               font=("Arial", 12, "bold"))
        title_label.pack(anchor="w", pady=(0, 10))
        
        # Informations de base
        info_frame = ttk.Frame(main_frame)
        info_frame.pack(fill="x", pady=(0, 10))
        
        ttk.Label(info_frame, text=f"{localization.t('col_domain')}: {domain}", 
                 font=("Arial", 10, "bold")).pack(anchor="w")
        ttk.Label(info_frame, text=f"{localization.t('col_module')}: {localization.translate_module(finding.check)}").pack(anchor="w")
        ttk.Label(info_frame, text=f"{localization.t('col_severity')}: {localization.translate_severity(finding.severity)}").pack(anchor="w")
        
        # Description (maintenant traduite)
        if hasattr(translated_finding, 'description') and translated_finding.description:
            desc_label = ttk.Label(main_frame, text=localization.t("finding_description"), 
                                  font=("Arial", 10, "bold"))
            desc_label.pack(anchor="w", pady=(10, 5))
            
            desc_text = scrolledtext.ScrolledText(main_frame, height=6, wrap=tk.WORD)
            desc_text.pack(fill="both", expand=True, pady=(0, 10))
            desc_text.insert("1.0", translated_finding.description)
            desc_text.config(state=tk.DISABLED)
        
        # Recommandation (maintenant traduite)
        if hasattr(translated_finding, 'remediation') and translated_finding.remediation:
            remed_label = ttk.Label(main_frame, text=localization.t("finding_remediation"), 
                                   font=("Arial", 10, "bold"))
            remed_label.pack(anchor="w", pady=(10, 5))
            
            remed_text = scrolledtext.ScrolledText(main_frame, height=4, wrap=tk.WORD)
            remed_text.pack(fill="both", expand=True, pady=(0, 10))
            remed_text.insert("1.0", translated_finding.remediation)
            remed_text.config(state=tk.DISABLED)
        
        # Preuves
        if finding.evidence:
            evidence_label = ttk.Label(main_frame, text=localization.t("finding_evidence"), 
                                     font=("Arial", 10, "bold"))
            evidence_label.pack(anchor="w", pady=(10, 5))
            
            evidence_text = scrolledtext.ScrolledText(main_frame, height=4, wrap=tk.WORD)
            evidence_text.pack(fill="both", expand=True, pady=(0, 10))
            evidence_text.insert("1.0", str(finding.evidence))
            evidence_text.config(state=tk.DISABLED)
        
        # Impact
        if finding.impact:
            impact_label = ttk.Label(main_frame, text=localization.t("finding_impact"), 
                                   font=("Arial", 10, "bold"))
            impact_label.pack(anchor="w", pady=(10, 5))
            
            impact_text = scrolledtext.ScrolledText(main_frame, height=3, wrap=tk.WORD)
            impact_text.pack(fill="both", expand=True, pady=(0, 10))
            impact_text.insert("1.0", finding.impact)
            impact_text.config(state=tk.DISABLED)
        
        # Bouton fermer
        ttk.Button(main_frame, text=localization.t("btn_close"), 
                  command=details_window.destroy).pack(pady=10)
        
        # Cette méthode fait déjà partie de show_finding_details, pas besoin de résumé ici
        
    def scan_error(self, error: str):
        """Erreur générale de scan."""
        self.scan_btn.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        self.progress_bar.stop()
        self.status_label.config(text=localization.t("status_error"))
        messagebox.showerror(localization.t("error_title"), f"{localization.t('error_scan_failed')} {error}")
        
    def stop_scan(self):
        """Arrêter le scan en cours."""
        self.is_scanning = False
        if self.current_scan_thread and self.current_scan_thread.is_alive():
            # Note: threading en Python ne permet pas d'arrêter proprement un thread
            # Cette fonctionnalité serait à améliorer avec des mécanismes de communication
            self.status_label.config(text=localization.t("status_stopped"))
        
        self.scan_btn.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        self.progress_bar.stop()
        
    def clear_results(self):
        """Effacer les résultats précédents."""
        for item in self.results_tree.get_children():
            self.results_tree.delete(item)
        self.scan_results.clear()
        self.summary_label.config(text="Scan en cours...")
        

            
    def export_report(self):
        """Exporter le rapport vers HTML/JSON."""
        if not self.scan_results:
            messagebox.showwarning(localization.t("attention_title"), 
                                 localization.t("no_scan_performed"))
            return
            
        filename = filedialog.asksaveasfilename(
            title=localization.t("btn_export"),
            defaultextension=".html",
            filetypes=[
                (localization.t("html_files"), "*.html"),
                (localization.t("json_files"), "*.json")
            ]
        )
        
        if filename:
            try:
                history_store = HistoryStore(Path.home() / ".web-sentinel" / "history.json")
                report_engine = ReportEngine(history_store=history_store)
                
                if filename.endswith('.json'):
                    # Export JSON
                    combined_findings = []
                    for result in self.scan_results:
                        combined_findings.extend(result.findings)
                    
                    context = report_engine.build_context(self.scan_results[0])  # Base context
                    with open(filename, 'w', encoding='utf-8') as f:
                        f.write(report_engine.render_json(context))
                else:
                    if not self.subscription_manager.can_export_html():
                        self.show_upgrade_prompt("html_export")
                        return
                    # Export HTML  
                    context = report_engine.build_context(self.scan_results[0])
                    with open(filename, 'w', encoding='utf-8') as f:
                        f.write(report_engine.render_html(context))
                        
                self.status_label.config(text=localization.t("success_export", Path(filename).name))
                
                # Proposer d'ouvrir le fichier
                if messagebox.askyesno(localization.t("export_success_title"), 
                                     localization.t("export_success_open")):
                    webbrowser.open(f"file://{Path(filename).absolute()}")
                    
            except Exception as e:
                messagebox.showerror(localization.t("error_title"), 
                                   localization.t("error_export_failed") + f": {e}")
        
    def run(self):
        """Lancer l'application."""
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.root.mainloop()
        
    def on_closing(self):
        """Gestionnaire de fermeture de l'application."""
        self.save_config()
        self.root.destroy()
    
    # === MÉTHODES SAST ===
    
    def _can_use_sast(self) -> bool:
        """Vérifier si l'utilisateur peut utiliser le SAST selon sa licence."""
        try:
            # Méthode 1: Via subscription_manager
            features = self.subscription_manager.get_features()
            if hasattr(features, 'allow_source_scan') and features.allow_source_scan:
                return True
            
            # Méthode 2: Vérification directe via le tier
            tier = self.subscription_manager.tier
            from ..subscription.models import get_features_for_tier
            direct_features = get_features_for_tier(tier)
            if direct_features.allow_source_scan:
                return True
                
            # Méthode 3: Vérification pour les comptes SYSOP (au cas où)
            if hasattr(self, 'auth_manager') and self.auth_manager.is_authenticated():
                user = self.auth_manager.get_current_user()
                if user and hasattr(user, 'email'):
                    # Comptes SYSOP ont toujours accès
                    sysop_domains = ['admin.local', 'sysop.local', 'dev.local']
                    if any(domain in user.email for domain in sysop_domains):
                        return True
            
            return False
        except Exception as e:
            # Debug pour comprendre le problème
            print(f"[DEBUG] _can_use_sast error: {e}")
            # Par défaut, autoriser pour SYSOP en cas d'erreur
            try:
                tier = self.subscription_manager.tier
                return tier.value.upper() in ['PRO', 'ENTERPRISE', 'SYSOP']
            except:
                return False
    
    def _on_module_toggle(self, module: str):
        """Gestionnaire de changement de module."""
        if module == 'source-code':
            if self.module_vars[module].get():
                # Activation du SAST - vérifier la licence
                if not self._can_use_sast():
                    self.module_vars[module].set(False)
                    messagebox.showwarning(
                        "Fonctionnalité premium", 
                        "L'analyse de code source (SAST) est disponible pour les licences PRO, ENTERPRISE et SYSOP uniquement."
                    )
                    return
                
                # Afficher la section SAST
                self.sast_frame.grid()
            else:
                # Masquer la section SAST
                self.sast_frame.grid_remove()
    
    def _browse_source_folder(self):
        """Ouvrir un dialogue de sélection de dossier pour le code source."""
        folder = filedialog.askdirectory(
            title=localization.t("dialog_select_source_folder"),
            initialdir=self.source_path_var.get() or os.getcwd()
        )
        if folder:
            self.source_path_var.set(folder)
    
    def _validate_sast_config(self) -> bool:
        """Valider la configuration SAST avant le scan."""
        if not self.module_vars.get('source-code', tk.BooleanVar()).get():
            return True  # SAST pas activé, pas de validation nécessaire
        
        if not self.source_path_var.get().strip():
            messagebox.showerror("Erreur SAST", "Veuillez sélectionner un dossier source.")
            return False
        
        source_path = Path(self.source_path_var.get().strip())
        if not source_path.exists():
            messagebox.showerror("Erreur SAST", "Le dossier source spécifié n'existe pas.")
            return False
        
        if not source_path.is_dir():
            messagebox.showerror("Erreur SAST", "Le chemin spécifié n'est pas un dossier.")
            return False
        
        return True


class UpgradeDialog:
    """Fenêtre de sélection d'offre d'abonnement."""

    def __init__(self, gui: "WebSentinelGUI", feature_message: str):
        self.gui = gui
        self.window = tk.Toplevel(gui.root)
        self.window.title(localization.t("subscription_upgrade_title"))
        self.window.transient(gui.root)
        self.window.grab_set()
        self.window.resizable(False, False)
        self.window.configure(padx=20, pady=20)

        header = ttk.Label(
            self.window,
            text=localization.t("subscription_upgrade_title"),
            font=("Arial", 14, "bold"),
        )
        header.pack(anchor="center", pady=(0, 5))

        subtitle = ttk.Label(
            self.window,
            text=localization.t("subscription_upgrade_subtitle"),
            font=("Arial", 10),
        )
        subtitle.pack(anchor="center", pady=(0, 15))

        feature_label = ttk.Label(
            self.window,
            text=feature_message,
            wraplength=420,
            justify=tk.CENTER,
        )
        feature_label.pack(anchor="center", pady=(0, 20))

        bg_main = "#f8fafc"
        border_color = "#e5e7eb"
        column_styles = {
            "feature": {"bg": bg_main, "fg": "#1f2937"},
            "free": {"bg": "#f3f4f6", "fg": "#111827"},
            "pro": {"bg": "#dbeafe", "fg": "#1d4ed8"},
            "enterprise": {"bg": "#ede9fe", "fg": "#6b21a8"},
        }

        comparison_frame = tk.Frame(self.window, bg=bg_main)
        comparison_frame.pack(fill=tk.BOTH, expand=True)
        for col in range(4):
            comparison_frame.grid_columnconfigure(col, weight=1)

        def place_cell(row, column, *, text=None, widget=None, bg=bg_main, fg="#1f2937", font=("Arial", 10), anchor="center", padding=(10, 8)):
            cell = tk.Frame(
                comparison_frame,
                bg=bg,
                highlightbackground=border_color,
                highlightthickness=1,
            )
            cell.grid(row=row, column=column, padx=4, pady=2, sticky="nsew")
            if text is not None:
                label = tk.Label(
                    cell,
                    text=text,
                    bg=bg,
                    fg=fg,
                    font=font,
                    anchor=anchor,
                    justify=tk.CENTER,
                    wraplength=170,
                )
                label.pack(fill=tk.BOTH, expand=True, padx=padding[0], pady=padding[1])
            elif widget is not None:
                cell.columnconfigure(0, weight=1)
                widget(cell)
            return cell

        place_cell(
            0,
            0,
            text=localization.t("subscription_table_feature_header"),
            bg=column_styles["feature"]["bg"],
            fg=column_styles["feature"]["fg"],
            font=("Arial", 10, "bold"),
            anchor="w",
        )

        plan_columns = [
            {
                "tier": "free",
                "title": localization.t("subscription_plan_free_title"),
                "prices": [localization.t("subscription_plan_free_price")],
                "monthly_value": 0.0,
                "annual_value": 0.0,
            },
            {
                "tier": "pro",
                "title": localization.t("subscription_plan_pro_title"),
                "prices": [
                    localization.t("subscription_plan_pro_price_monthly"),
                    localization.t("subscription_plan_pro_price_annual"),
                ],
                "monthly_value": 9.99,
                "annual_value": 99.0,
            },
            {
                "tier": "enterprise",
                "title": localization.t("subscription_plan_enterprise_title"),
                "prices": [
                    localization.t("subscription_plan_enterprise_price_monthly"),
                    localization.t("subscription_plan_enterprise_price_annual"),
                ],
                "monthly_value": 49.0,
                "annual_value": 490.0,
            },
        ]

        for col_index, plan in enumerate(plan_columns, start=1):
            style = column_styles[plan["tier"]]
            place_cell(
                0,
                col_index,
                text=plan["title"],
                bg=style["bg"],
                fg=style["fg"],
                font=("Arial", 11, "bold"),
            )
            place_cell(
                1,
                col_index,
                text="\n".join(plan["prices"]),
                bg=style["bg"],
                fg=style["fg"],
                font=("Arial", 9),
            )

            discount_text = ""
            if plan["monthly_value"] and plan["annual_value"]:
                monthly_total = plan["monthly_value"] * 12
                if monthly_total > plan["annual_value"]:
                    discount = int(round((1 - plan["annual_value"] / monthly_total) * 100))
                    if discount > 0:
                        discount_text = localization.t("subscription_discount_format", discount)
            place_cell(
                2,
                col_index,
                text=discount_text,
                bg=style["bg"],
                fg=style["fg"],
                font=("Arial", 9, "bold"),
            )

        place_cell(
            1,
            0,
            text=localization.t("subscription_pricing_label"),
            bg=column_styles["feature"]["bg"],
            fg=column_styles["feature"]["fg"],
            font=("Arial", 10, "bold"),
            anchor="w",
        )
        place_cell(
            2,
            0,
            text=localization.t("subscription_discount_label"),
            bg=column_styles["feature"]["bg"],
            fg=column_styles["feature"]["fg"],
            font=("Arial", 10, "bold"),
            anchor="w",
        )

        ttk.Separator(comparison_frame, orient=tk.HORIZONTAL).grid(
            row=3, column=0, columnspan=4, sticky="ew", pady=(6, 8)
        )

        feature_rows = [
            (
                localization.t("subscription_feature_scan_passive"),
                {"free": True, "pro": True, "enterprise": True},
            ),
            (
                localization.t("subscription_feature_invasive"),
                {"free": False, "pro": True, "enterprise": True},
            ),
            (
                localization.t("subscription_feature_html_export"),
                {"free": False, "pro": True, "enterprise": True},
            ),
            (
                localization.t("subscription_feature_multi_user"),
                {"free": False, "pro": False, "enterprise": True},
            ),
            (
                localization.t("subscription_feature_api"),
                {"free": False, "pro": False, "enterprise": True},
            ),
        ]

        feature_start_row = 4
        for row_offset, (feature_label_text, availability) in enumerate(feature_rows, start=feature_start_row):
            place_cell(
                row_offset,
                0,
                text=feature_label_text,
                bg=column_styles["feature"]["bg"],
                fg=column_styles["feature"]["fg"],
                anchor="w",
            )

            for col_index, plan in enumerate(plan_columns, start=1):
                style = column_styles[plan["tier"]]
                enabled = availability.get(plan["tier"], False)
                symbol = "✓" if enabled else "✗"
                color = "#047857" if enabled else "#b91c1c"
                place_cell(
                    row_offset,
                    col_index,
                    text=symbol,
                    bg=style["bg"],
                    fg=color,
                    font=("Arial", 14, "bold"),
                )

        cta_row = feature_start_row + len(feature_rows)

        def pro_buttons(cell):
            ttk.Button(
                cell,
                text=localization.t("subscription_select_monthly"),
                command=lambda: self._checkout("pro", "monthly"),
                style="Accent.TButton",
            ).pack(fill=tk.X, pady=(0, 4))
            ttk.Button(
                cell,
                text=localization.t("subscription_select_annual"),
                command=lambda: self._checkout("pro", "annual"),
            ).pack(fill=tk.X)

        def enterprise_buttons(cell):
            ttk.Button(
                cell,
                text=localization.t("subscription_select_monthly"),
                command=lambda: self._checkout("enterprise", "monthly"),
                style="Accent.TButton",
            ).pack(fill=tk.X, pady=(0, 4))
            ttk.Button(
                cell,
                text=localization.t("subscription_select_annual"),
                command=lambda: self._checkout("enterprise", "annual"),
            ).pack(fill=tk.X)

        place_cell(cta_row, 1, text="", bg=column_styles["free"]["bg"], fg=column_styles["free"]["fg"])
        place_cell(cta_row, 2, widget=pro_buttons, bg=column_styles["pro"]["bg"], fg=column_styles["pro"]["fg"])
        place_cell(cta_row, 3, widget=enterprise_buttons, bg=column_styles["enterprise"]["bg"], fg=column_styles["enterprise"]["fg"])

        ttk.Button(
            self.window,
            text=localization.t("btn_close"),
            command=self.window.destroy,
        ).pack(pady=(20, 0))

        self._center_window()

    def _checkout(self, tier: str, cadence: str) -> None:
        url = self.gui.open_checkout(tier, cadence)
        if url:
            self.window.destroy()

    def _center_window(self) -> None:
        self.window.update_idletasks()
        width = self.window.winfo_width()
        height = self.window.winfo_height()
        if not width:
            width = 760
        if not height:
            height = 560
        x = (self.window.winfo_screenwidth() // 2) - (width // 2)
        y = (self.window.winfo_screenheight() // 2) - (height // 2)
        self.window.geometry(f"{width}x{height}+{x}+{y}")


def main():
    """Point d'entrée de l'interface graphique."""
    app = WebSentinelGUI()
    app.run()


if __name__ == "__main__":
    main()
