"""
Gestionnaire de raccourcis pour intercepter et remplacer les mots-clés
"""
import keyboard
import time
import re
import os
import sys
import base64
import requests
from pathlib import Path
from typing import List, Dict, Optional, Callable, Tuple
from datetime import datetime
from urllib.parse import urlparse
import hashlib
import json
import tkinter as tk
from tkinter import ttk
import threading

class ShortcutHandler:
    """Gère l'interception et le remplacement des raccourcis"""
    
    def __init__(self, api_client, config, on_shortcut_found: Optional[Callable] = None):
        self.api_client = api_client
        self.config = config
        self.on_shortcut_found = on_shortcut_found
        
        self.shortcuts_cache: List[Dict] = []
        self.is_enabled = config.get('enabled', True)
        self.is_replacing = False
        self.current_text = ""
        self.last_key_time = 0
        self.check_timeout = None
        
        # Buffer pour capturer le texte tapé au fur et à mesure (commence par /)
        self.text_buffer = ""
        self.is_capturing = False  # Indique si on est en train de capturer un raccourci
        self.buffer_start_time = 0
        self.max_buffer_length = 50  # Limite du buffer pour les raccourcis
        
        # Cache pour les listes de formulaire (préchargées au démarrage)
        # Format: {list_id: {'items': [...], 'loaded_at': timestamp}}
        self.form_lists_cache: Dict[int, Dict] = {}
        
        # Créer le dossier pour stocker les images localement
        self.images_dir = self._get_images_directory()
        os.makedirs(self.images_dir, exist_ok=True)
        
        # Charger les raccourcis en arrière-plan au démarrage (préchargera aussi les listes)
        # pour ne pas bloquer le lancement de l'application.
        threading.Thread(target=self.reload_shortcuts, daemon=True).start()
    
    def _get_images_directory(self) -> Path:
        """Retourne le chemin du dossier images selon le mode d'exécution"""
        if getattr(sys, 'frozen', False):
            # Mode exécutable - utiliser le dossier de l'exécutable
            base_dir = Path(sys.executable).parent
        elif Path(__file__).parent.name == 'app':
            # Mode portable
            base_dir = Path(__file__).parent.parent
        else:
            # Mode développement
            base_dir = Path(__file__).parent
        
        return base_dir / 'images'
    
    def reload_shortcuts(self):
        """Recharge les raccourcis depuis l'API et précharge les listes de formulaire utilisées"""
        client_id = self.config.get('client_id')
        group_id = self.config.get('group_id')
        self.shortcuts_cache = self.api_client.get_shortcuts(client_id, group_id)
        print(f"[MyMind] {len(self.shortcuts_cache)} raccourcis chargés")
        if self.shortcuts_cache:
            print(f"[MyMind Debug] Exemples de raccourcis: {[s.get('keyword') for s in self.shortcuts_cache[:3]]}")
        
        # Précharger les listes de formulaire utilisées par les raccourcis
        self.preload_form_lists()
    
    def preload_form_lists(self):
        """Précharge toutes les listes de formulaire utilisées par les raccourcis en cache"""
        # Collecter tous les list_id uniques depuis les form_fields des raccourcis
        list_ids_to_load = set()
        for shortcut in self.shortcuts_cache:
            form_fields = shortcut.get('form_fields')
            if form_fields and isinstance(form_fields, list):
                for field in form_fields:
                    if field.get('type') == 'list' and field.get('list_id'):
                        list_ids_to_load.add(field['list_id'])
        
        if not list_ids_to_load:
            print("[MyMind] Aucune liste de formulaire à précharger")
            return
        
        print(f"[MyMind] Préchargement de {len(list_ids_to_load)} liste(s) de formulaire...")
        
        # Charger chaque liste en arrière-plan (threading pour ne pas bloquer)
        def load_list(list_id: int):
            try:
                list_data = self.api_client.get_form_list_items(list_id)
                if list_data and isinstance(list_data, dict) and 'items' in list_data:
                    items = list_data['items']
                    # Mettre en cache uniquement si on a des données valides (thread-safe)
                    self.form_lists_cache[list_id] = {
                        'items': items if isinstance(items, list) else [],
                        'loaded_at': time.time()
                    }
                    print(f"[MyMind] ✓ Liste {list_id} préchargée ({len(items)} éléments)")
                else:
                    # Liste vide - mettre en cache avec liste vide
                    self.form_lists_cache[list_id] = {
                        'items': [],
                        'loaded_at': time.time()
                    }
                    print(f"[MyMind] Liste {list_id} préchargée (vide)")
            except Exception as e:
                print(f"[MyMind] Erreur lors du préchargement de la liste {list_id}: {e}")
                # IMPORTANT: Ne pas mettre en cache en cas d'erreur, pour permettre un rechargement depuis l'API
                # On laisse la liste hors du cache pour qu'elle soit rechargée au moment de l'utilisation
                pass
        
        # Charger toutes les listes en parallèle (mais attendre la fin)
        threads = []
        for list_id in list_ids_to_load:
            thread = threading.Thread(target=load_list, args=(list_id,), daemon=True)
            thread.start()
            threads.append(thread)
        
        # Attendre que tous les threads soient terminés (avec timeout)
        # IMPORTANT: Attendre la fin pour s'assurer que le cache est rempli avant utilisation
        for i, thread in enumerate(threads):
            thread.join(timeout=10.0)  # Timeout de 10 secondes par liste
            if thread.is_alive():
                print(f"[MyMind] ATTENTION: Le chargement de la liste a pris plus de 10 secondes")
        
        # Vérifier combien de listes ont été chargées avec succès
        loaded_count = len(self.form_lists_cache)
        print(f"[MyMind] Préchargement terminé: {loaded_count} liste(s) en cache sur {len(list_ids_to_load)} demandées")
        
        # Afficher les listes manquantes pour debug
        if loaded_count < len(list_ids_to_load):
            missing = list_ids_to_load - set(self.form_lists_cache.keys())
            print(f"[MyMind] Listes non chargées: {missing}")
    
    def set_enabled(self, enabled: bool):
        """Active ou désactive le gestionnaire"""
        self.is_enabled = enabled
        self.config.set('enabled', enabled)
    
    def has_potential_shortcut_prefix(self, text: str) -> bool:
        """Vérifie si le texte pourrait contenir un préfixe de raccourci"""
        if not text or not self.shortcuts_cache:
            return False
        
        # Vérifier si le texte se termine par un préfixe de raccourci
        # Par exemple, si on a tapé "/a", on vérifie si un raccourci commence par "/a"
        text_clean = text.rstrip()
        
        # Vérifier si le texte se termine par un préfixe de n'importe quel raccourci
        for shortcut in self.shortcuts_cache:
            keyword = shortcut.get('keyword', '')
            if not keyword:
                continue
            
            # Si le texte se termine par le début d'un mot-clé, c'est un préfixe potentiel
            if text_clean.endswith(keyword):
                return True
            # Vérifier si le texte se termine par un préfixe du mot-clé
            if len(text_clean) > 0 and keyword.startswith(text_clean):
                return True
            # Vérifier si le texte contient le mot-clé à la fin (avec des caractères avant)
            if text_clean.endswith(keyword):
                return True
        
        return False
    
    def find_shortcut(self, text: str) -> Optional[Dict]:
        """Trouve le raccourci correspondant dans le texte"""
        if not text:
            return None
        
        if not self.shortcuts_cache:
            print("[MyMind Debug] Aucun raccourci dans le cache")
            return None
        
        # Chercher le raccourci le plus long qui correspond
        matched_shortcut = None
        max_length = 0
        
        # Nettoyer le texte (enlever les espaces en fin)
        text_clean = text.rstrip()
        
        for shortcut in self.shortcuts_cache:
            keyword = shortcut.get('keyword', '')
            if not keyword:
                continue
            
            # Vérifier si le texte se termine par le mot-clé
            # Essayer avec et sans espace à la fin
            if text_clean.endswith(keyword) and len(keyword) > max_length:
                # Vérifier qu'il n'y a pas de caractère alphanumérique juste avant
                before_keyword = text_clean[:len(text_clean) - len(keyword)]
                last_char = before_keyword[-1] if before_keyword else None
                if not last_char or not re.match(r'[a-zA-Z0-9]', last_char):
                    matched_shortcut = shortcut
                    max_length = len(keyword)
        
        return matched_shortcut
    
    def show_form_fields_dialog(self, fields: List[Dict], shortcut: Dict) -> Optional[Dict[str, str]]:
        """
        Affiche une fenêtre Tkinter pour remplir les champs du formulaire
        Retourne un dictionnaire {placeholder: value} ou None si l'utilisateur annule
        """
        # Obtenir ou créer la fenêtre racine Tkinter
        # Si une fenêtre principale existe déjà, utiliser Toplevel
        # Sinon, créer une nouvelle racine
        try:
            root_window = tk._default_root
            if root_window and root_window.winfo_exists():
                # Une fenêtre principale existe, utiliser Toplevel
                root = tk.Toplevel(root_window)
            else:
                # Aucune fenêtre principale, créer une nouvelle
                root = tk.Tk()
        except:
            # En cas d'erreur, créer une nouvelle fenêtre
            root = tk.Tk()
        
        # Style moderne - thème sombre cohérent avec le client portable
        bg_color = '#101317'          # Fond principal sombre
        secondary_bg = '#151a21'      # Cartes / blocs
        text_color = '#f9fafb'        # Texte clair
        primary_color = '#3b82f6'     # Bleu principal (titre, bouton Valider)
        secondary_color = '#4b5563'   # Gris pour bouton Annuler
        border_color = '#1f2933'
        
        root.title("Formulaire de saisie")
        root.configure(bg=bg_color)
        
        # Taille dynamique de la fenêtre en fonction du nombre de champs
        try:
            root.update_idletasks()
            screen_w = root.winfo_screenwidth()
            screen_h = root.winfo_screenheight()
            nb_fields = max(1, len(fields))
            base_height = 120           # footer
            per_field = 75              # hauteur moyenne par champ
            target_height = base_height + per_field * nb_fields
            max_height = int(screen_h * 0.85)
            height = min(max_height, target_height)
            width = min(650, max(480, int(screen_w * 0.38)))
            x = (screen_w // 2) - (width // 2)
            y = (screen_h // 2) - (height // 2)
            root.geometry(f"{width}x{height}+{x}+{y}")
            root.minsize(450, 250)
        except Exception:
            root.geometry("550x400")
        
        # Variables pour stocker les valeurs
        field_values = {}
        widgets = {}
        
        # Liste pour stocker les fenêtres de suggestions flottantes (pour les fermer proprement)
        suggestion_popups = []
        
        # Container principal
        inner_main = tk.Frame(root, bg=bg_color)
        inner_main.pack(fill='both', expand=True, padx=8, pady=8)
        
        # Frame principal avec scrollbar pour le contenu
        # IMPORTANT: Ne pas packer le content_frame maintenant, on le fera après avoir créé tous les widgets
        content_frame = tk.Frame(inner_main, bg=bg_color)
        
        canvas = tk.Canvas(content_frame, bg=bg_color, highlightthickness=0)
        scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview)
        scrollable_frame = tk.Frame(canvas, bg=bg_color)
        
        print(f"[MyMind Dialog] Canvas et scrollable_frame créés")
        
        scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )
        
        canvas_window = canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        
        # Configurer le redimensionnement du canvas
        def configure_scroll_region(event):
            canvas.configure(scrollregion=canvas.bbox("all"))
            # Ajuster la largeur du scrollable_frame à celle du canvas
            canvas_width = event.width
            canvas.itemconfig(canvas_window, width=canvas_width)
        
        canvas.bind('<Configure>', configure_scroll_region)
        
        print(f"[MyMind Dialog] Début - Nombre de champs à créer: {len(fields)}")
        print(f"[MyMind Dialog] Champs reçus: {fields}")
        
        # Cache pour les éléments de liste (utiliser le cache préchargé si disponible)
        list_items_cache = {}
        # Dictionnaire pour stocker les widgets de chargement (progressbar + label) par list_id
        loading_widgets = {}

        def open_full_list_popup(all_options: Dict[str, Dict], var: tk.StringVar):
            """
            Ouvre une fenêtre flottante indépendante qui affiche toute la liste des éléments.
            Utilisée notamment quand l'utilisateur tape '*', pour éviter que la liste ne soit tronquée
            par le contenu scrollable du formulaire.
            """
            if not all_options:
                return

            popup = tk.Toplevel(root)
            popup.title("Sélection")
            popup.configure(bg=bg_color)
            popup.attributes('-topmost', True)

            # Placer la fenêtre au centre de l'écran
            popup.update_idletasks()
            sw = popup.winfo_screenwidth()
            sh = popup.winfo_screenheight()
            width = max(400, min(700, int(sw * 0.5)))
            height = min(600, int(sh * 0.7))
            x = (sw // 2) - (width // 2)
            y = (sh // 2) - (height // 2)
            popup.geometry(f"{width}x{height}+{x}+{y}")

            title_label = tk.Label(
                popup,
                text="Sélectionner un élément",
                font=("Segoe UI", 11, "bold"),
                bg=bg_color,
                fg=text_color,
                pady=6
            )
            title_label.pack(fill="x")

            listbox = tk.Listbox(
                popup,
                font=("Segoe UI", 10),
                bg='white',
                fg='#111827',
                selectbackground=primary_color,
                selectforeground='white',
                activestyle='none'
            )
            listbox.pack(fill="both", expand=True, padx=10, pady=8)

            # Remplir avec tous les titres (sans ascenseur explicite : c'est la fenêtre qui porte la hauteur)
            titles = list(all_options.keys())
            for title in titles:
                listbox.insert(tk.END, title)

            def select_full_list_item(index):
                """Sélectionne l'élément à l'index donné"""
                if index >= 0 and index < listbox.size():
                    selected_title = listbox.get(index)
                    print(f"[MyMind] Sélection popup complète: index={index}, valeur='{selected_title}'")
                    var.set(selected_title)
                    popup.destroy()

            def validate_selection(event=None):
                sel = listbox.curselection()
                if sel:
                    select_full_list_item(sel[0])

            def on_single_click(event):
                """Sélectionne l'élément au simple clic (utilise nearest)"""
                index = listbox.nearest(event.y)
                if index >= 0:
                    select_full_list_item(index)

            def on_escape(event=None):
                popup.destroy()

            listbox.bind("<Button-1>", on_single_click)
            listbox.bind("<Return>", validate_selection)
            popup.bind("<Escape>", on_escape)

            # Focus sur la liste
            listbox.focus_set()
        
        # Pré-remplir le cache avec les données préchargées
        print(f"[MyMind Dialog] Vérification du cache: {len(self.form_lists_cache)} liste(s) en cache")
        for field in fields:
            if field.get('type') == 'list' and field.get('list_id'):
                list_id = field['list_id']
                print(f"[MyMind Dialog] Vérification liste {list_id} dans cache: {list_id in self.form_lists_cache}")
                if list_id in self.form_lists_cache:
                    cached_data = self.form_lists_cache[list_id]
                    items = cached_data.get('items', [])
                    list_items_cache[list_id] = items
                    print(f"[MyMind Dialog] Liste {list_id} chargée depuis le cache ({len(items)} éléments)")
                else:
                    print(f"[MyMind Dialog] Liste {list_id} NON trouvée dans le cache, sera chargée depuis l'API")
        
        print(f"[MyMind Dialog] Création de {len(fields)} champ(s)...")
        # Créer les champs (mise en page plus condensée)
        for index, field in enumerate(fields):
            placeholder = field.get('placeholder', '')
            # Normaliser le placeholder (enlever les accolades si présentes)
            placeholder_normalized = placeholder.strip('{}')
            
            # Frame pour chaque champ avec bordure et padding moderne mais compacte
            field_frame = tk.Frame(
                scrollable_frame, 
                bg=secondary_bg, 
                relief='flat',
                borderwidth=1
            )
            field_frame.pack(fill='x', pady=4, padx=2)
            
            # Padding interne (réduit)
            inner_frame = tk.Frame(field_frame, bg=secondary_bg)
            inner_frame.pack(fill='x', padx=10, pady=8)
            
            # Label moderne
            label_text = field.get('label', placeholder)
            label = tk.Label(
                inner_frame,
                text=label_text,
                font=("Segoe UI", 10, "bold"),
                bg=secondary_bg,
                fg=text_color,
                anchor='w'
            )
            label.pack(fill='x', pady=(0, 4))
            
            # Widget selon le type
            if field.get('type') == 'list' and field.get('list_id'):
                # Liste déroulante
                list_id = field['list_id']
                items = list_items_cache.get(list_id, None)  # None = pas encore chargé
                
                print(f"[MyMind] Création du champ liste pour {label_text}, liste {list_id}")
                
                var = tk.StringVar()
                
                # Créer un Frame pour le Combobox avec style
                combobox_frame = tk.Frame(inner_frame, bg=secondary_bg)
                combobox_frame.pack(fill='x', pady=(0, 2))
                
                if items is not None and len(items) > 0:
                    # Créer un widget de recherche avec autocomplétion (Entry + popup flottante)
                    print(f"[MyMind] Création d'un champ de recherche pour {label_text} avec {len(items)} éléments")
                    
                    # Frame pour le champ de recherche
                    search_container = tk.Frame(combobox_frame, bg=secondary_bg)
                    search_container.pack(fill='x')
                    
                    # Entry pour la recherche
                    search_entry = tk.Entry(
                        search_container,
                        textvariable=var,
                        font=("Segoe UI", 10),
                        bg='white',
                        fg='#111827',
                        relief='solid',
                        borderwidth=1,
                        highlightthickness=0,
                        insertbackground='#111827'
                    )
                    search_entry.pack(fill='x', ipady=8)
                    
                    # Stocker toutes les options (titre -> item complet)
                    all_options = {str(item.get('title', '')): item for item in items if item.get('title')}
                    
                    # Variable pour stocker la fenêtre popup de suggestions
                    suggestion_popup = {'window': None, 'listbox': None}
                    
                    def show_suggestion_popup(filtered_items):
                        """Affiche une fenêtre flottante avec les suggestions sous le champ"""
                        # Fermer l'ancienne popup si elle existe
                        hide_suggestion_popup()
                        
                        if not filtered_items:
                            return
                        
                        # Créer une fenêtre Toplevel sans barre de titre
                        popup = tk.Toplevel(root)
                        popup.overrideredirect(True)
                        popup.configure(bg=border_color)
                        popup.attributes('-topmost', True)
                        suggestion_popups.append(popup)
                        
                        # Forcer la mise à jour pour obtenir les bonnes coordonnées
                        root.update_idletasks()
                        search_entry.update_idletasks()
                        
                        # Obtenir la position absolue du champ de saisie sur l'écran
                        entry_x = search_entry.winfo_rootx()
                        entry_y = search_entry.winfo_rooty()
                        entry_width = search_entry.winfo_width()
                        entry_height = search_entry.winfo_height()
                        
                        # Hauteur de la popup (max 8 éléments, ~24px par ligne)
                        num_items = min(len(filtered_items), 8)
                        popup_height = num_items * 24 + 6
                        
                        # Positionner juste en dessous du champ de saisie
                        popup_x = entry_x
                        popup_y = entry_y + entry_height + 2
                        
                        # Si la popup dépasse en bas de l'écran, la mettre au-dessus
                        screen_height = root.winfo_screenheight()
                        if popup_y + popup_height > screen_height:
                            popup_y = entry_y - popup_height - 2
                        
                        popup.geometry(f"{entry_width}x{popup_height}+{popup_x}+{popup_y}")
                        
                        # Frame intérieur
                        inner_popup = tk.Frame(popup, bg='white')
                        inner_popup.pack(fill='both', expand=True, padx=1, pady=1)
                        
                        # Listbox pour les suggestions
                        listbox = tk.Listbox(
                            inner_popup,
                            font=("Segoe UI", 10),
                            bg='white',
                            fg='#111827',
                            relief='flat',
                            borderwidth=0,
                            selectbackground=primary_color,
                            selectforeground='white',
                            activestyle='none',
                            highlightthickness=0
                        )
                        listbox.pack(fill='both', expand=True)
                        
                        for item in filtered_items[:8]:
                            listbox.insert(tk.END, item)
                        
                        def select_item(index):
                            """Sélectionne l'élément à l'index donné"""
                            if index >= 0 and index < listbox.size():
                                selected_value = listbox.get(index)
                                print(f"[MyMind] Sélection: index={index}, valeur='{selected_value}'")
                                var.set(selected_value)
                                hide_suggestion_popup()
                                # Ne pas appeler check_and_validate() ici pour éviter la fermeture auto
                        
                        def on_popup_select(event=None):
                            """Sélection via clavier (Enter)"""
                            sel = listbox.curselection()
                            if sel:
                                select_item(sel[0])
                        
                        def on_popup_key(event):
                            if event.keysym == 'Return':
                                on_popup_select()
                            elif event.keysym == 'Escape':
                                hide_suggestion_popup()
                                search_entry.focus_set()
                            elif event.keysym == 'Up' and listbox.curselection() and listbox.curselection()[0] == 0:
                                hide_suggestion_popup()
                                search_entry.focus_set()
                        
                        def on_click(event):
                            """Sélectionne l'élément au clic (utilise nearest pour fiabilité)"""
                            # Trouver l'index de l'élément le plus proche de la position du clic
                            index = listbox.nearest(event.y)
                            if index >= 0:
                                select_item(index)
                        
                        listbox.bind('<Button-1>', on_click)
                        listbox.bind('<Return>', lambda e: on_popup_select())
                        listbox.bind('<KeyPress>', on_popup_key)
                        
                        # Stocker les références
                        suggestion_popup['window'] = popup
                        suggestion_popup['listbox'] = listbox
                        
                        # Garder au premier plan
                        popup.lift()
                    
                    def hide_suggestion_popup():
                        """Ferme la popup de suggestions"""
                        if suggestion_popup['window']:
                            try:
                                suggestion_popup['window'].destroy()
                            except:
                                pass
                            suggestion_popup['window'] = None
                            suggestion_popup['listbox'] = None
                    
                    def filter_suggestions(event=None):
                        """Filtre les suggestions selon le texte saisi"""
                        search_text = var.get()
                        search_text_lower = search_text.lower() if search_text else ''
                        
                        # Si l'utilisateur tape "*", ouvrir la fenêtre complète
                        if search_text == '*':
                            hide_suggestion_popup()
                            open_full_list_popup(all_options, var)
                            return
                        
                        # Filtrer les options
                        filtered = [
                            title for title in all_options.keys()
                            if search_text_lower in title.lower()
                        ]
                        
                        if search_text and filtered:
                            # Trier par pertinence
                            filtered.sort(key=lambda x: (
                                0 if x.lower().startswith(search_text_lower) else 1,
                                x.lower().find(search_text_lower)
                            ))
                            show_suggestion_popup(filtered[:20])
                        else:
                            hide_suggestion_popup()
                    
                    def on_entry_key(event):
                        """Gère les touches dans le champ de recherche"""
                        if event.keysym == 'Down':
                            # Flèche bas : focus sur la listbox popup
                            if suggestion_popup['listbox']:
                                suggestion_popup['listbox'].focus_set()
                                suggestion_popup['listbox'].selection_set(0)
                            return
                        elif event.keysym == 'Return':
                            # Entrée : sélectionner le premier résultat ou valider
                            if suggestion_popup['listbox'] and suggestion_popup['listbox'].size() > 0:
                                suggestion_popup['listbox'].selection_set(0)
                                var.set(suggestion_popup['listbox'].get(0))
                                hide_suggestion_popup()
                                check_and_validate()
                            else:
                                check_and_validate()
                            return
                        elif event.keysym == 'Escape':
                            hide_suggestion_popup()
                            return
                        else:
                            # Autre touche : filtrer (y compris gestion de "*")
                            root.after_idle(filter_suggestions)
                    
                    def on_focus_out(event):
                        """Ferme la popup quand le champ perd le focus"""
                        # Délai pour permettre le clic sur la popup
                        root.after(150, lambda: hide_suggestion_popup() if root.focus_get() != search_entry else None)
                    
                    # Bindings
                    search_entry.bind('<KeyRelease>', filter_suggestions)
                    search_entry.bind('<KeyPress>', on_entry_key)
                    search_entry.bind('<FocusOut>', on_focus_out)
                    
                    # Stocker les références
                    widgets[placeholder_normalized] = {
                        'widget': search_container,
                        'entry': search_entry,
                        'var': var,
                        'type': 'list',
                        'items': items,
                        'all_options': all_options,
                        'suggestion_popup': suggestion_popup,
                        'hide_popup': hide_suggestion_popup
                    }
                    
                    print(f"[MyMind] Champ de recherche créé avec {len(items)} éléments pour {label_text}")
                elif items is not None and len(items) == 0:
                    # Si pas d'éléments mais que la liste était en cache, 
                    # peut-être que le chargement a échoué - recharger depuis l'API
                    # Pour l'instant, afficher un label désactivé mais permettre le rechargement
                    print(f"[MyMind] Liste {list_id} en cache mais vide - affichage de la barre de chargement pour recharger")
                    # Traiter comme si items était None pour forcer un rechargement depuis l'API
                    items = None  # Forcer le rechargement depuis l'API
                    
                    # Créer la barre de progression comme si items était None
                    loading_frame = tk.Frame(combobox_frame, bg=secondary_bg)
                    loading_frame.pack(fill='x')
                    
                    # Label "Chargement..."
                    loading_label = tk.Label(
                        loading_frame,
                        text="Chargement de la liste...",
                        font=("Segoe UI", 10),
                        bg=secondary_bg,
                        fg=text_color,
                        anchor='w'
                    )
                    loading_label.pack(fill='x', pady=(0, 5))
                    
                    # Barre de progression
                    progressbar = ttk.Progressbar(
                        loading_frame,
                        mode='indeterminate',
                        length=400,
                        style='TProgressbar'
                    )
                    progressbar.pack(fill='x', pady=(0, 0))
                    progressbar.start(10)  # Démarrer l'animation
                    
                    # Stocker les références pour mise à jour ultérieure
                    loading_widgets[list_id] = {
                        'frame': loading_frame,
                        'label': loading_label,
                        'progressbar': progressbar,
                        'placeholder': placeholder_normalized,
                        'var': var,
                        'combobox_frame': combobox_frame
                    }
                    
                    widgets[placeholder_normalized] = {
                        'widget': loading_frame,
                        'var': var,
                        'type': 'list',
                        'items': None,  # Pas encore chargé
                        'loading': True
                    }
                    print(f"[MyMind] Widget de chargement créé pour liste {list_id} (cache vide)")
                else:
                    # Items == None : en cours de chargement, afficher une barre de progression
                    loading_frame = tk.Frame(combobox_frame, bg=secondary_bg)
                    loading_frame.pack(fill='x')
                    
                    # Label "Chargement..."
                    loading_label = tk.Label(
                        loading_frame,
                        text="Chargement de la liste...",
                        font=("Segoe UI", 10),
                        bg=secondary_bg,
                        fg=text_color,
                        anchor='w'
                    )
                    loading_label.pack(fill='x', pady=(0, 5))
                    
                    # Barre de progression
                    progressbar = ttk.Progressbar(
                        loading_frame,
                        mode='indeterminate',
                        length=400,
                        style='TProgressbar'
                    )
                    progressbar.pack(fill='x', pady=(0, 0))
                    progressbar.start(10)  # Démarrer l'animation
                    
                    # Stocker les références pour mise à jour ultérieure
                    loading_widgets[list_id] = {
                        'frame': loading_frame,
                        'label': loading_label,
                        'progressbar': progressbar,
                        'placeholder': placeholder_normalized,
                        'var': var,
                        'combobox_frame': combobox_frame
                    }
                    
                    widgets[placeholder_normalized] = {
                        'widget': loading_frame,
                        'var': var,
                        'type': 'list',
                        'items': None,  # Pas encore chargé
                        'loading': True
                    }
                    print(f"[MyMind] Widget de chargement créé pour liste {list_id}")
            else:
                # Champ de saisie texte moderne
                entry_frame = tk.Frame(inner_frame, bg=secondary_bg)
                entry_frame.pack(fill='x')
                
                var = tk.StringVar()
                entry = tk.Entry(
                    entry_frame,
                    textvariable=var,
                    font=("Segoe UI", 10),
                    bg='white',
                    fg='#111827',  # texte sombre
                    relief='solid',
                    borderwidth=1,
                    highlightthickness=0,
                    insertbackground='#111827'
                )
                entry.pack(fill='x', ipady=8)
                
                # Binding Enter pour valider automatiquement quand tous les champs sont remplis
                def on_entry_return(event):
                    check_and_validate()
                
                entry.bind('<Return>', on_entry_return)
                
                widgets[placeholder_normalized] = {
                    'widget': entry,
                    'var': var,
                    'type': 'input'
                }
            
            print(f"[MyMind Dialog] Champ {index+1}/{len(fields)} créé: {placeholder_normalized} (type: {field.get('type')})")
        
        print(f"[MyMind Dialog] Total de widgets créés: {len(widgets)}")
        
        # Fonction pour charger les listes en arrière-plan et mettre à jour les widgets
        def load_list_and_update_widget(list_id: int, loading_widget_info: dict):
            """Charge une liste depuis le cache ou l'API et met à jour le widget correspondant"""
            # Vérifier d'abord le cache
            if list_id in self.form_lists_cache:
                cached_data = self.form_lists_cache[list_id]
                items = cached_data.get('items', [])
                print(f"[MyMind] Liste {list_id} chargée depuis le cache ({len(items)} éléments)")
                # Mettre à jour le cache local de la fonction
                list_items_cache[list_id] = items
                # Mettre à jour l'UI immédiatement
                update_ui_with_items(list_id, items, loading_widget_info)
                return
            
            # Si pas en cache, charger depuis l'API
            print(f"[MyMind] Chargement de la liste {list_id} depuis l'API...")
            try:
                list_data = self.api_client.get_form_list_items(list_id)
                print(f"[MyMind Dialog] Réponse API pour liste {list_id}: {list_data}")
                
                items = []
                if list_data is None:
                    print(f"[MyMind Dialog] ERREUR: list_data est None pour la liste {list_id}")
                    # En cas d'erreur, on met une liste vide pour permettre la validation
                    items = []
                elif isinstance(list_data, dict) and 'items' in list_data:
                    items = list_data['items'] if isinstance(list_data['items'], list) else []
                    if len(items) > 0:
                        print(f"[MyMind Dialog] ✓ {len(items)} éléments chargés pour la liste {list_id}")
                    else:
                        print(f"[MyMind Dialog] ATTENTION: La liste {list_id} contient 0 éléments")
                    # Mettre à jour le cache global
                    self.form_lists_cache[list_id] = {
                        'items': items,
                        'loaded_at': time.time()
                    }
                else:
                    print(f"[MyMind Dialog] ERREUR: Format de réponse inattendu pour la liste {list_id}")
                    items = []  # Liste vide en cas d'erreur de format
                
                # Mettre en cache même si vide (pour éviter de recharger indéfiniment)
                if list_id not in self.form_lists_cache:
                    self.form_lists_cache[list_id] = {
                        'items': items,
                        'loaded_at': time.time()
                    }
                
                # Mettre à jour le cache local
                list_items_cache[list_id] = items
                
                # Mettre à jour l'interface dans le thread principal
                update_ui_with_items(list_id, items, loading_widget_info)
                
            except Exception as e:
                print(f"[MyMind Dialog] Exception lors du chargement de la liste {list_id}: {e}")
                import traceback
                traceback.print_exc()
                # En cas d'erreur, afficher un message d'erreur dans l'UI mais permettre la validation
                def show_error():
                    try:
                        loading_frame = loading_widget_info['frame']
                        loading_label = loading_widget_info['label']
                        progressbar = loading_widget_info['progressbar']
                        var = loading_widget_info['var']
                        combobox_frame = loading_widget_info['combobox_frame']
                        placeholder = loading_widget_info['placeholder']
                        
                        progressbar.stop()
                        loading_frame.destroy()
                        
                        # Créer un champ de saisie libre pour permettre la saisie manuelle
                        search_container = tk.Frame(combobox_frame, bg=secondary_bg)
                        search_container.pack(fill='x')
                        
                        error_label = tk.Label(
                            search_container,
                            text="⚠ Liste non chargée - Saisie manuelle possible",
                            font=("Segoe UI", 9),
                            bg=secondary_bg,
                            fg='#fbbf24',
                            anchor='w',
                            pady=(0, 4)
                        )
                        error_label.pack(fill='x')
                        
                        # Entry pour saisie manuelle
                        search_entry = tk.Entry(
                            search_container,
                            textvariable=var,
                            font=("Segoe UI", 10),
                            bg='white',
                            fg='#111827',
                            relief='solid',
                            borderwidth=1,
                            highlightthickness=0,
                            insertbackground='#111827'
                        )
                        search_entry.pack(fill='x', ipady=8)
                        
                        list_items_cache[list_id] = []
                        widgets[placeholder] = {
                            'widget': search_container,
                            'entry': search_entry,
                            'var': var,
                            'type': 'list',
                            'items': [],  # Liste vide mais chargée
                            'loading': False,  # IMPORTANT: marquer comme non-loading
                            'all_options': {}  # Pas d'options mais on permet la saisie
                        }
                        print(f"[MyMind] Widget mis à jour en mode dégradé pour liste {list_id}")
                    except Exception as e2:
                        print(f"[MyMind Dialog] Erreur lors de l'affichage de l'erreur: {e2}")
                
                root.after(0, show_error)
        
        # Fonction pour mettre à jour l'UI avec les items chargés
        def update_ui_with_items(list_id: int, items: List[Dict], loading_widget_info: dict):
            """Met à jour l'interface utilisateur avec les items chargés"""
            def update_ui():
                    try:
                        loading_frame = loading_widget_info['frame']
                        loading_label = loading_widget_info['label']
                        progressbar = loading_widget_info['progressbar']
                        var = loading_widget_info['var']
                        combobox_frame = loading_widget_info['combobox_frame']
                        placeholder = loading_widget_info['placeholder']
                        
                        # Arrêter et détruire la barre de progression
                        progressbar.stop()
                        loading_frame.destroy()
                        
                        if items and len(items) > 0:
                            # Créer un widget de recherche avec popup flottante
                            print(f"[MyMind] Mise à jour avec un champ de recherche avec {len(items)} éléments")
                            
                            # Frame pour le champ de recherche
                            search_container = tk.Frame(combobox_frame, bg=secondary_bg)
                            search_container.pack(fill='x')
                            
                            # Entry pour la recherche
                            search_entry = tk.Entry(
                                search_container,
                                textvariable=var,
                                font=("Segoe UI", 10),
                                bg='white',
                                fg='#111827',
                                relief='solid',
                                borderwidth=1,
                                highlightthickness=0,
                                insertbackground='#111827'
                            )
                            search_entry.pack(fill='x', ipady=8)
                            
                            # Stocker toutes les options (titre -> item complet)
                            all_options = {str(item.get('title', '')): item for item in items if item.get('title')}
                            
                            # Variable pour stocker la fenêtre popup de suggestions
                            suggestion_popup_async = {'window': None, 'listbox': None}
                            
                            def show_suggestion_popup_async(filtered_items):
                                """Affiche une fenêtre flottante avec les suggestions sous le champ"""
                                hide_suggestion_popup_async()
                                if not filtered_items:
                                    return
                                
                                popup = tk.Toplevel(root)
                                popup.overrideredirect(True)
                                popup.configure(bg=border_color)
                                popup.attributes('-topmost', True)
                                suggestion_popups.append(popup)
                                
                                # Forcer la mise à jour pour obtenir les bonnes coordonnées
                                root.update_idletasks()
                                search_entry.update_idletasks()
                                
                                # Obtenir la position absolue du champ de saisie sur l'écran
                                entry_x = search_entry.winfo_rootx()
                                entry_y = search_entry.winfo_rooty()
                                entry_width = search_entry.winfo_width()
                                entry_height = search_entry.winfo_height()
                                
                                # Hauteur de la popup (max 8 éléments, ~24px par ligne)
                                num_items = min(len(filtered_items), 8)
                                popup_height = num_items * 24 + 6
                                
                                # Positionner juste en dessous du champ de saisie
                                popup_x = entry_x
                                popup_y = entry_y + entry_height + 2
                                
                                # Si la popup dépasse en bas de l'écran, la mettre au-dessus
                                screen_height = root.winfo_screenheight()
                                if popup_y + popup_height > screen_height:
                                    popup_y = entry_y - popup_height - 2
                                
                                popup.geometry(f"{entry_width}x{popup_height}+{popup_x}+{popup_y}")
                                
                                inner_popup = tk.Frame(popup, bg='white')
                                inner_popup.pack(fill='both', expand=True, padx=1, pady=1)
                                
                                listbox = tk.Listbox(
                                    inner_popup,
                                    font=("Segoe UI", 10),
                                    bg='white',
                                    fg='#111827',
                                    relief='flat',
                                    borderwidth=0,
                                    selectbackground=primary_color,
                                    selectforeground='white',
                                    activestyle='none',
                                    highlightthickness=0
                                )
                                listbox.pack(fill='both', expand=True)
                                
                                for item_title in filtered_items[:8]:
                                    listbox.insert(tk.END, item_title)
                                
                                def select_item_async(index):
                                    """Sélectionne l'élément à l'index donné"""
                                    if index >= 0 and index < listbox.size():
                                        selected_value = listbox.get(index)
                                        print(f"[MyMind] Sélection async: index={index}, valeur='{selected_value}'")
                                        var.set(selected_value)
                                        hide_suggestion_popup_async()
                                
                                def on_popup_select_async(event=None):
                                    """Sélection via clavier (Enter)"""
                                    sel = listbox.curselection()
                                    if sel:
                                        select_item_async(sel[0])
                                
                                def on_click_async(event):
                                    """Sélectionne l'élément au clic (utilise nearest pour fiabilité)"""
                                    index = listbox.nearest(event.y)
                                    if index >= 0:
                                        select_item_async(index)
                                
                                listbox.bind('<Button-1>', on_click_async)
                                listbox.bind('<Return>', lambda e: on_popup_select_async())
                                
                                suggestion_popup_async['window'] = popup
                                suggestion_popup_async['listbox'] = listbox
                                popup.lift()
                            
                            def hide_suggestion_popup_async():
                                if suggestion_popup_async['window']:
                                    try:
                                        suggestion_popup_async['window'].destroy()
                                    except:
                                        pass
                                    suggestion_popup_async['window'] = None
                                    suggestion_popup_async['listbox'] = None
                            
                            def filter_suggestions_async(event=None):
                                """Filtre les suggestions selon le texte saisi (version async)"""
                                search_text = var.get()
                                search_text_lower = search_text.lower() if search_text else ''
                                
                                if search_text == '*':
                                    hide_suggestion_popup_async()
                                    open_full_list_popup(all_options, var)
                                    return
                                
                                filtered = [
                                    title for title in all_options.keys()
                                    if search_text_lower in title.lower()
                                ]
                                
                                if search_text and filtered:
                                    filtered.sort(key=lambda x: (
                                        0 if x.lower().startswith(search_text_lower) else 1,
                                        x.lower().find(search_text_lower)
                                    ))
                                    show_suggestion_popup_async(filtered[:20])
                                else:
                                    hide_suggestion_popup_async()
                            
                            def on_entry_key_async(event):
                                """Gère les touches dans le champ de recherche"""
                                if event.keysym == 'Down':
                                    if suggestion_popup_async['listbox']:
                                        suggestion_popup_async['listbox'].focus_set()
                                        suggestion_popup_async['listbox'].selection_set(0)
                                    return
                                elif event.keysym == 'Return':
                                    if suggestion_popup_async['listbox'] and suggestion_popup_async['listbox'].size() > 0:
                                        suggestion_popup_async['listbox'].selection_set(0)
                                        var.set(suggestion_popup_async['listbox'].get(0))
                                        hide_suggestion_popup_async()
                                        check_and_validate()
                                    else:
                                        check_and_validate()
                                    return
                                elif event.keysym == 'Escape':
                                    hide_suggestion_popup_async()
                                    return
                                else:
                                    root.after_idle(filter_suggestions_async)
                            
                            # Bindings
                            search_entry.bind('<KeyRelease>', filter_suggestions_async)
                            search_entry.bind('<KeyPress>', on_entry_key_async)
                            
                            # Mettre à jour le cache et le widget dans le dictionnaire
                            list_items_cache[list_id] = items
                            widgets[placeholder] = {
                                'widget': search_container,
                                'entry': search_entry,
                                'var': var,
                                'type': 'list',
                                'items': items,
                                'all_options': all_options,
                                'suggestion_popup': suggestion_popup_async,
                                'hide_popup': hide_suggestion_popup_async
                            }
                            
                            print(f"[MyMind] Champ de recherche mis à jour avec {len(items)} éléments pour liste {list_id}")
                        else:
                            # Aucun élément disponible
                            disabled_label = tk.Label(
                                combobox_frame,
                                text="-- Aucun élément disponible --",
                                font=("Segoe UI", 10),
                                bg='#e9ecef',
                                fg='#6c757d',
                                relief='solid',
                                borderwidth=1,
                                anchor='w',
                                padx=10,
                                pady=8
                            )
                            disabled_label.pack(fill='x')
                            
                            list_items_cache[list_id] = []
                            widgets[placeholder] = {
                                'widget': disabled_label,
                                'var': var,
                                'type': 'list',
                                'items': []
                            }
                            print(f"[MyMind] Menu déroulant mis à jour sans éléments pour liste {list_id}")
                        
                        # Forcer la mise à jour de la scrollregion
                        root.update_idletasks()
                        canvas.update_idletasks()
                        bbox = canvas.bbox("all")
                        if bbox:
                            canvas.configure(scrollregion=bbox)
                        
                        # Forcer le redessin
                        root.update()
                    except Exception as e:
                        print(f"[MyMind Dialog] Erreur lors de la mise à jour de l'UI pour liste {list_id}: {e}")
                        import traceback
                        traceback.print_exc()
            
            # Planifier la mise à jour dans le thread principal (Tkinter n'est pas thread-safe)
            root.after(0, update_ui)
        
        # Démarrer le chargement asynchrone des listes
        if loading_widgets:
            print(f"[MyMind Dialog] Démarrage du chargement asynchrone de {len(loading_widgets)} liste(s)...")
            for list_id, loading_widget_info in loading_widgets.items():
                # Lancer le chargement dans un thread séparé
                thread = threading.Thread(
                    target=load_list_and_update_widget,
                    args=(list_id, loading_widget_info),
                    daemon=True
                )
                thread.start()
        
        # Vérifier qu'on a bien créé des widgets
        if len(widgets) == 0:
            # Afficher un message d'erreur si aucun champ n'a été créé
            error_label = tk.Label(
                scrollable_frame,
                text="Erreur: Aucun champ à afficher",
                font=("Segoe UI", 12),
                bg=bg_color,
                fg='red',
                pady=20
            )
            error_label.pack()
            print("[MyMind Dialog] ERREUR: Aucun widget créé, affichage d'un message d'erreur")
        
        # IMPORTANT: Packer le canvas et la scrollbar dans content_frame
        print(f"[MyMind Dialog] Packing du canvas et de la scrollbar dans content_frame...")
        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
        # Packer le content_frame dans main_frame (AVANT le footer)
        print(f"[MyMind Dialog] Packing du content_frame dans main_frame...")
        content_frame.pack(fill='both', expand=True, padx=20, pady=20)
        
        # Mettre à jour la scrollregion immédiatement
        root.update_idletasks()
        scrollable_frame.update_idletasks()
        canvas.update_idletasks()
        bbox = canvas.bbox("all")
        if bbox:
            canvas.configure(scrollregion=bbox)
            print(f"[MyMind Dialog] Scrollregion initiale: {bbox}")
        else:
            print(f"[MyMind Dialog] ATTENTION: bbox initiale est None")
        
        # Boutons dans un footer séparé (packé APRÈS le content_frame)
        footer_frame = tk.Frame(inner_main, bg=bg_color, height=70)
        footer_frame.pack(fill='x', side='bottom')  # Packer en dernier
        footer_frame.pack_propagate(False)
        
        button_container = tk.Frame(footer_frame, bg=bg_color)
        button_container.pack(expand=True)
        
        result = {'cancelled': True}
        
        def close_all_popups():
            """Ferme toutes les fenêtres popup de suggestions"""
            for popup in suggestion_popups:
                try:
                    popup.destroy()
                except:
                    pass
            suggestion_popups.clear()
        
        def collect_values():
            """Collecte toutes les valeurs des champs"""
            values = {}
            for placeholder_normalized, widget_info in widgets.items():
                # Vérifier si c'est un champ en cours de chargement
                # Si loading est True ET items est None, alors on attend
                # Mais si items est une liste vide [], on peut continuer (liste chargée mais vide)
                if widget_info.get('loading', False) and widget_info.get('items') is None:
                    return None  # Pas encore prêt
                
                if widget_info['type'] == 'list':
                    # Pour les listes, même si vide, on peut collecter la valeur
                    # Si items est None, c'est qu'il y a eu une erreur - on permet quand même la validation
                    # avec la valeur saisie manuellement
                    all_options = widget_info.get('all_options', {})
                    selected_title = widget_info['var'].get()
                    # Trouver la valeur correspondante
                    value = ''
                    if selected_title and all_options and selected_title in all_options:
                        value = all_options[selected_title].get('value', '')
                    elif selected_title:
                        # Si l'utilisateur a saisi quelque chose mais que la liste n'est pas chargée,
                        # on utilise directement ce qu'il a saisi (mode dégradé)
                        value = selected_title
                    values[placeholder_normalized] = value
                    print(f"[MyMind] Champ {placeholder_normalized} (liste): '{selected_title}' -> '{value}'")
                else:
                    value = widget_info['var'].get()
                    values[placeholder_normalized] = value
                    print(f"[MyMind] Champ {placeholder_normalized} (texte): '{value}'")
            return values
        
        def check_and_validate():
            """Vérifie si tous les champs sont remplis et valide automatiquement"""
            values = collect_values()
            if values is None:
                # Certains champs ne sont pas encore prêts (en cours de chargement)
                return
            
            # Vérifier que tous les champs obligatoires sont remplis
            # On permet la validation même si une liste est vide (l'utilisateur peut saisir manuellement)
            all_filled = True
            for placeholder_normalized, widget_info in widgets.items():
                # Si loading est True ET items est None, on attend encore
                if widget_info.get('loading', False) and widget_info.get('items') is None:
                    all_filled = False
                    break
                # Vérifier que le champ a une valeur (même vide pour les listes, on permet)
                if placeholder_normalized not in values:
                    all_filled = False
                    break
            
            if all_filled:
                # Tous les champs sont remplis, valider automatiquement
                nonlocal result
                result = values
                result['cancelled'] = False
                close_all_popups()
                root.destroy()
        
        def on_validate():
            """Valide manuellement (appelée par Enter sur le dernier champ)"""
            values = collect_values()
            if values is None:
                import tkinter.messagebox as messagebox
                messagebox.showwarning(
                    "Chargement en cours",
                    "Veuillez attendre que toutes les listes soient chargées."
                )
                return
            
            nonlocal result
            result = values
            result['cancelled'] = False
            close_all_popups()
            root.destroy()
        
        def close_all_popups():
            """Ferme toutes les fenêtres popup de suggestions"""
            for popup in suggestion_popups:
                try:
                    popup.destroy()
                except:
                    pass
            suggestion_popups.clear()
        
        def on_cancel():
            close_all_popups()
            root.destroy()
        
        # Bouton Valider
        validate_btn = tk.Button(
            button_container,
            text="Valider",
            command=on_validate,
            bg=primary_color,
            fg='white',
            font=("Segoe UI", 10, "bold"),
            padx=30,
            pady=10,
            cursor='hand2',
            relief='flat',
            borderwidth=0,
            activebackground='#5568d3',
            activeforeground='white'
        )
        validate_btn.pack(side='right', padx=5)
        
        # Bouton Annuler
        cancel_btn = tk.Button(
            button_container,
            text="Annuler",
            command=on_cancel,
            bg=secondary_color,
            fg='white',
            font=("Segoe UI", 10, "bold"),
            padx=30,
            pady=10,
            cursor='hand2',
            relief='flat',
            borderwidth=0,
            activebackground='#5a6268',
            activeforeground='white'
        )
        cancel_btn.pack(side='right', padx=5)
        
        # Forcer la mise à jour de l'affichage complet IMMÉDIATEMENT
        # CRITIQUE: Avec pythonw.exe (sans console), il faut forcer plusieurs updates
        # car la boucle d'événements ne se déclenche pas automatiquement
        print(f"[MyMind Dialog] Forçage des mises à jour pour pythonw.exe...")
        for i in range(5):
            root.update()
            root.update_idletasks()
            scrollable_frame.update_idletasks()
            canvas.update_idletasks()
            content_frame.update_idletasks()
        
        # Mettre à jour la scrollregion immédiatement
        bbox = canvas.bbox("all")
        if bbox:
            canvas.configure(scrollregion=bbox)
            print(f"[MyMind Dialog] Scrollregion initiale: {bbox}")
        else:
            print(f"[MyMind Dialog] ATTENTION: bbox initiale est None")
        
        # Vérifications de débogage
        canvas_width = canvas.winfo_width()
        canvas_height = canvas.winfo_height()
        scrollable_width = scrollable_frame.winfo_width()
        scrollable_height = scrollable_frame.winfo_height()
        print(f"[MyMind Dialog] Fenêtre configurée:")
        print(f"  - widgets créés: {len(widgets)}")
        print(f"  - canvas size: {canvas_width}x{canvas_height}")
        print(f"  - scrollable_frame size: {scrollable_width}x{scrollable_height}")
        
        # Focus sur le premier champ
        if widgets:
            first_widget = list(widgets.values())[0]['widget']
            try:
                first_widget.focus()
                if isinstance(first_widget, tk.Entry):
                    first_widget.select_range(0, tk.END)
            except Exception as e:
                print(f"[MyMind Dialog] Erreur lors du focus: {e}")
        
        # Gérer Escape pour annuler
        root.bind('<Escape>', lambda e: on_cancel())
        
        # Modal - bloquer toutes les interactions avec les autres fenêtres
        try:
            root.transient()
        except:
            pass  # Si transient échoue, continuer quand même
        
        root.grab_set()
        root.focus_force()
        
        # Forcer l'affichage et mettre au premier plan (critique pour pythonw.exe)
        root.deiconify()
        root.lift()
        root.attributes('-topmost', True)
        
        # Forcer encore plusieurs mises à jour après l'affichage
        for i in range(3):
            root.update()
            root.update_idletasks()
            canvas.update_idletasks()
            scrollable_frame.update_idletasks()
        
        # Mettre à jour la scrollregion après les updates
        bbox = canvas.bbox("all")
        if bbox:
            canvas.configure(scrollregion=bbox)
            print(f"[MyMind Dialog] Scrollregion après affichage: {bbox}")
        
        # Fonction pour finaliser l'affichage (appelée avec délai)
        def finalize_display():
            # Forcer une dernière série de mises à jour
            for i in range(3):
                root.update()
                root.update_idletasks()
                canvas.update_idletasks()
                scrollable_frame.update_idletasks()
            
            # Mettre à jour la scrollregion
            bbox = canvas.bbox("all")
            if bbox:
                canvas.configure(scrollregion=bbox)
                print(f"[MyMind Dialog] Scrollregion finalisée: {bbox}")
            
            # Enlever -topmost mais garder au premier plan
            root.attributes('-topmost', False)
            root.lift()
        
        # Appeler finalize_display plusieurs fois avec des délais croissants
        # Cela garantit que même avec pythonw.exe, les widgets finissent par s'afficher
        root.after(100, finalize_display)
        root.after(300, finalize_display)
        root.after(500, finalize_display)  # Après 500ms, normalement tout devrait être affiché
        
        # CRITIQUE pour pythonw.exe: Forcer la boucle d'événements à traiter tous les événements
        # en attente avant d'appeler wait_window(). Sans cela, les widgets peuvent ne pas s'afficher.
        # On fait plusieurs cycles de update() pour forcer le rendu immédiat
        print("[MyMind Dialog] Forçage final de la boucle d'événements avant wait_window()...")
        for cycle in range(20):  # 20 cycles pour être sûr
            root.update_idletasks()
            root.update()
        
        # Vérifier que la fenêtre est bien visible
        if not root.winfo_viewable():
            print("[MyMind Dialog] ATTENTION: Fenêtre non visible, forçage...")
            root.deiconify()
            root.lift()
            root.focus_force()
            root.attributes('-topmost', True)
            root.update()
            root.after(100, lambda: root.attributes('-topmost', False))
        else:
            print("[MyMind Dialog] Fenêtre visible, prête pour wait_window()")
        
        # Encore quelques cycles après vérification
        for cycle in range(10):
            root.update_idletasks()
            root.update()
        
        # Maintenant on peut appeler wait_window()
        root.wait_window()
        
        if result.get('cancelled'):
            return None
        
        # Retourner les valeurs avec les placeholders originaux (avec accolades)
        return_result = {}
        for placeholder_normalized, value in result.items():
            if placeholder_normalized != 'cancelled':
                # Trouver le placeholder original dans fields
                original_placeholder = None
                for field in fields:
                    field_placeholder = field.get('placeholder', '').strip('{}')
                    if field_placeholder == placeholder_normalized:
                        original_placeholder = field.get('placeholder', '')
                        # S'assurer qu'il y a des accolades
                        if not original_placeholder.startswith('{'):
                            original_placeholder = '{' + original_placeholder + '}'
                        break
                if original_placeholder:
                    return_result[original_placeholder] = value
        
        return return_result
    
    def replace_form_fields(self, text: str, shortcut: Dict) -> Optional[str]:
        """
        Remplace les placeholders de formulaire ({Form1}, {Form2}, etc.) dans le texte
        Affiche une fenêtre pour remplir les champs avant de remplacer
        Retourne le texte avec les placeholders remplacés, ou None si l'utilisateur annule
        """
        if not text or not shortcut:
            return text
        
        # Récupérer les champs de formulaire
        form_fields_json = shortcut.get('form_fields')
        if not form_fields_json:
            return text
        
        try:
            if isinstance(form_fields_json, str):
                form_fields = json.loads(form_fields_json)
            else:
                form_fields = form_fields_json
        except (json.JSONDecodeError, TypeError):
            print(f"[MyMind] Erreur lors du parsing de form_fields: {form_fields_json}")
            return text
        
        if not form_fields or not isinstance(form_fields, list):
            return text
        
        # Trouver tous les placeholders de formulaire dans le texte
        # Pattern: {Form1}, {Form2}, etc. (insensible à la casse)
        form_pattern = r'\{Form\d+\}'
        matches = re.findall(form_pattern, text, re.IGNORECASE)
        
        if not matches:
            return text
        
        # Extraire les champs correspondants
        fields_to_fill = []
        placeholders_found = set()
        
        for match in matches:
            # Normaliser le placeholder (enlever les accolades pour la comparaison)
            placeholder_normalized = match.strip('{}')
            
            # Trouver le champ correspondant dans form_fields
            for field in form_fields:
                field_placeholder = field.get('placeholder', '').strip('{}')
                if field_placeholder.lower() == placeholder_normalized.lower():
                    # Éviter les doublons
                    if match not in placeholders_found:
                        fields_to_fill.append(field)
                        placeholders_found.add(match)
                    break
        
        if not fields_to_fill:
            print(f"[MyMind] Aucun champ de formulaire trouvé pour les placeholders: {matches}")
            return text
        
        # Afficher la fenêtre de formulaire
        print(f"[MyMind] Affichage de la fenêtre de formulaire pour {len(fields_to_fill)} champ(s)")
        form_values = self.show_form_fields_dialog(fields_to_fill, shortcut)
        
        if not form_values:
            print("[MyMind] L'utilisateur a annulé le remplissage du formulaire")
            return None  # L'utilisateur a annulé
        
        # Remplacer les placeholders par les valeurs
        result = text
        for original_placeholder, value in form_values.items():
            # Utiliser une regex pour remplacer (insensible à la casse)
            pattern = re.escape(original_placeholder)
            result = re.sub(pattern, value, result, flags=re.IGNORECASE)
        
        print(f"[MyMind] Placeholders remplacés: {list(form_values.keys())}")
        return result
    
    def replace_special_commands(self, text: str) -> Tuple[str, Optional[int]]:
        """
        Remplace les commandes spéciales (date, heure) dans le texte
        Retourne (texte, position_curseur) où position_curseur est la position où placer le curseur
        (-1 si pas de {curseur}, ou l'index de la position où était {curseur})
        """
        if not text:
            return text, None
        
        now = datetime.now()
        
        # Format de date : JJ/MM/AAAA
        date_str = now.strftime('%d/%m/%Y')
        
        # Format d'heure : HH:MM
        time_str = now.strftime('%H:%M')
        
        # Format date et heure : JJ/MM/AAAA HH:MM
        date_time_str = f"{date_str} {time_str}"
        
        # Vérifier si le texte contient {curseur}
        cursor_pos = None
        result = text
        
        # Trouver la position de {curseur} (insensible à la casse)
        cursor_match = re.search(r'\{curseur\}', result, re.IGNORECASE)
        if cursor_match:
            cursor_pos = cursor_match.start()
            # Retirer {curseur} du texte
            result = re.sub(r'\{curseur\}', '', result, flags=re.IGNORECASE)
        
        # Remplacer les autres commandes (insensible à la casse)
        result = re.sub(r'\{date\}', date_str, result, flags=re.IGNORECASE)
        result = re.sub(r'\{heure\}|\{time\}', time_str, result, flags=re.IGNORECASE)
        result = re.sub(r'\{datetime\}', date_time_str, result, flags=re.IGNORECASE)
        
        return result, cursor_pos
    
    def save_base64_image(self, base64_data: str) -> Optional[str]:
        """Sauvegarde une image base64 localement"""
        if not base64_data:
            return None
        
        try:
            # Détecter le format (data:image/png;base64,... ou juste base64)
            if base64_data.startswith('data:'):
                # Extraire le type MIME et les données
                header, data = base64_data.split(',', 1)
                mime_match = re.search(r'data:image/([^;]+)', header)
                if mime_match:
                    image_type = mime_match.group(1)
                else:
                    image_type = 'png'
            else:
                # Données base64 brutes
                data = base64_data
                image_type = 'png'
            
            # Décoder les données base64
            image_bytes = base64.b64decode(data)
            
            # Créer un hash des données pour le nom de fichier
            data_hash = hashlib.md5(image_bytes).hexdigest()
            file_ext = f'.{image_type}' if image_type else '.png'
            
            # Nom du fichier local
            local_filename = f"{data_hash}{file_ext}"
            local_path = self.images_dir / local_filename
            
            # Si l'image existe déjà, retourner le chemin
            if local_path.exists():
                print(f"[MyMind Debug] Image base64 deja sauvegardee: {local_path}")
                return str(local_path)
            
            # Sauvegarder l'image
            with open(local_path, 'wb') as f:
                f.write(image_bytes)
            
            print(f"[MyMind Debug] Image base64 sauvegardee: {local_path}")
            return str(local_path)
        
        except Exception as e:
            print(f"[MyMind Debug] Erreur lors de la sauvegarde de l'image base64: {e}")
            return None
    
    def download_image(self, image_url: str) -> Optional[str]:
        """Télécharge une image depuis l'URL et la stocke localement"""
        if not image_url:
            return None
        
        # Vérifier si c'est une image base64
        if image_url.startswith('data:image/'):
            return self.save_base64_image(image_url)
        
        try:
            # Créer un hash de l'URL pour le nom de fichier
            url_hash = hashlib.md5(image_url.encode()).hexdigest()
            parsed_url = urlparse(image_url)
            file_ext = os.path.splitext(parsed_url.path)[1] or '.png'
            
            # Nom du fichier local
            local_filename = f"{url_hash}{file_ext}"
            local_path = self.images_dir / local_filename
            
            # Si l'image existe déjà, retourner le chemin
            if local_path.exists():
                print(f"[MyMind Debug] Image deja telechargee: {local_path}")
                return str(local_path)
            
            # Télécharger l'image
            print(f"[MyMind Debug] Telechargement de l'image: {image_url}")
            response = requests.get(image_url, timeout=10)
            response.raise_for_status()
            
            # Sauvegarder l'image
            with open(local_path, 'wb') as f:
                f.write(response.content)
            
            print(f"[MyMind Debug] Image telechargee: {local_path}")
            return str(local_path)
        
        except Exception as e:
            print(f"[MyMind Debug] Erreur lors du telechargement de l'image: {e}")
            return None
    
    def copy_html_to_clipboard(self, html_text: str, plain_text: str = None) -> bool:
        """
        Copie du HTML formaté dans le presse-papiers Windows (préserve la mise en forme)
        Copie aussi le texte brut pour compatibilité avec les applications comme Notepad
        """
        try:
            import win32clipboard
        except ImportError as e:
            print(f"[MyMind Debug] ERREUR: pywin32 non disponible pour copier le HTML formaté!")
            print(f"[MyMind Debug] Pour installer: pip install pywin32")
            print(f"[MyMind Debug] Erreur detaillee: {e}")
            return False
        
        try:
            # Extraire le texte brut du HTML si non fourni
            if plain_text is None:
                # Utiliser strip_html pour extraire le texte brut
                plain_text = self.strip_html(html_text)
                # Normaliser les sauts de ligne
                plain_text = plain_text.replace('\r\n', '\n').replace('\r', '\n')
                plain_text = re.sub(r'\n{3,}', '\n\n', plain_text)
            
            # Format HTML du presse-papiers Windows nécessite un en-tête spécial
            # Format: Version:0.9\r\nStartHTML:0000000000\r\nEndHTML:0000000000\r\nStartFragment:0000000000\r\nEndFragment:0000000000\r\n<html>...fragment...</html>
            
            # Créer le fragment HTML
            html_body = f"<html><body>{html_text}</body></html>"
            
            # Créer l'en-tête avec des placeholders (10 chiffres pour chaque position)
            header_template = "Version:0.9\r\nStartHTML:0000000000\r\nEndHTML:0000000000\r\nStartFragment:0000000000\r\nEndFragment:0000000000\r\n"
            
            # Calculer les positions réelles
            start_html = len(header_template)
            start_fragment = start_html + len("<html><body>")
            end_fragment = start_fragment + len(html_text)
            end_html = end_fragment + len("</body></html>")
            
            # Créer l'en-tête avec les positions réelles
            header = f"Version:0.9\r\nStartHTML:{start_html:010d}\r\nEndHTML:{end_html:010d}\r\nStartFragment:{start_fragment:010d}\r\nEndFragment:{end_fragment:010d}\r\n"
            
            # Assembler le tout
            clipboard_data = header + html_body
            
            # Copier dans le presse-papiers avec plusieurs formats
            win32clipboard.OpenClipboard()
            win32clipboard.EmptyClipboard()
            
            # Copier le format HTML (pour Word/Outlook/etc.)
            win32clipboard.SetClipboardData(win32clipboard.RegisterClipboardFormat("HTML Format"), clipboard_data.encode('utf-8'))
            
            # Copier aussi le texte brut (pour Notepad/etc.)
            win32clipboard.SetClipboardData(win32clipboard.CF_TEXT, plain_text.encode('utf-8'))
            
            # Copier aussi en Unicode (CF_UNICODETEXT = 13)
            win32clipboard.SetClipboardData(win32clipboard.CF_UNICODETEXT, plain_text)
            
            win32clipboard.CloseClipboard()
            
            print(f"[MyMind Debug] HTML formate et texte brut copies dans le presse-papiers (HTML: {len(html_text)}, texte: {len(plain_text)})")
            return True
        except Exception as e:
            print(f"[MyMind Debug] Erreur lors de la copie du HTML formate: {e}")
            import traceback
            traceback.print_exc()
            return False
    
    def copy_image_to_clipboard(self, image_path: str) -> bool:
        """Copie une image dans le presse-papiers Windows"""
        try:
            from PIL import Image
            import win32clipboard
            from io import BytesIO
        except ImportError as e:
            print(f"[MyMind Debug] ERREUR: Pillow ou pywin32 non disponible!")
            print(f"[MyMind Debug] Pour installer: pip install Pillow pywin32")
            print(f"[MyMind Debug] Erreur detaillee: {e}")
            return False
        
        try:
            # Ouvrir l'image
            img = Image.open(image_path)
            
            # Convertir en format compatible avec le presse-papiers Windows
            output = BytesIO()
            img.convert("RGB").save(output, "BMP")
            data = output.getvalue()[14:]  # Enlever l'en-tête BMP
            output.close()
            
            # Copier dans le presse-papiers
            win32clipboard.OpenClipboard()
            win32clipboard.EmptyClipboard()
            win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
            win32clipboard.CloseClipboard()
            
            print(f"[MyMind Debug] Image copiee dans le presse-papiers: {image_path}")
            return True
        except Exception as e:
            print(f"[MyMind Debug] Erreur lors de la copie de l'image: {e}")
            import traceback
            traceback.print_exc()
            return False
    
    def clean_html_preserve_formatting(self, html_text: str) -> str:
        """
        Nettoie le HTML en préservant la mise en forme (gras, italique, couleur, etc.)
        Retire seulement les attributs de <p> mais garde toutes les balises de formatage
        """
        if not html_text:
            return ""
        
        processed_html = html_text
        
        # Retirer les attributs des balises <p> (comme class="MsoNormal") mais garder <p></p>
        processed_html = re.sub(r'<p[^>]*>', '<p>', processed_html, flags=re.IGNORECASE)
        
        # Préserver les espaces vides intentionnels
        processed_html = re.sub(r'</p>\s*<p>\s*<br\s*/?>\s*</p>\s*<p>', '</p><p><br></p><p>', processed_html, flags=re.IGNORECASE)
        
        # Supprimer les balises <p> vides complètement (sans <br>)
        processed_html = re.sub(r'<p>\s*</p>', '', processed_html, flags=re.IGNORECASE)
        
        return processed_html
    
    def process_html_with_images(self, html_text: str, shortcut: Dict) -> Tuple[str, str, List[Tuple[int, str]]]:
        """
        Traite le HTML en téléchargeant les images et en convertissant en texte formaté
        Retourne (texte formaté, HTML nettoyé avec mise en forme, liste de positions d'images)
        """
        if not html_text:
            return "", "", []
        
        try:
            import html2text
        except ImportError:
            print("[MyMind Debug] html2text non disponible, utilisation de la conversion simple")
            cleaned_html = self.clean_html_preserve_formatting(html_text)
            return self.strip_html(html_text), cleaned_html, []
        
        # Télécharger toutes les images trouvées dans le HTML
        processed_html = html_text
        image_positions = []  # Liste de (position_dans_html, chemin_local)
        
        # Trouver toutes les balises <img> dans le HTML (y compris avec data URI)
        img_tag_pattern = r'<img[^>]*>'
        img_matches = list(re.finditer(img_tag_pattern, processed_html, re.IGNORECASE))
        
        # Traiter les matches en ordre inverse pour préserver les positions lors du remplacement
        for match in reversed(img_matches):
            img_tag = match.group(0)
            # Extraire le src
            src_match = re.search(r'src\s*=\s*(["\']?)([^\s>]+?)\1', img_tag, re.IGNORECASE)
            if src_match:
                img_url = src_match.group(2)
                if img_url:
                    local_path = self.download_image(img_url)
                    if local_path:
                        # Stocker la position dans le HTML original (avant conversion)
                        position_in_html = match.start()
                        image_positions.append((position_in_html, local_path))
                        # Remplacer la balise <img> par un marqueur temporaire
                        marker = f"__IMAGE_MARKER_{len(image_positions)-1}__"
                        start, end = match.span()
                        processed_html = processed_html[:start] + marker + processed_html[end:]
        
        # Télécharger aussi l'image du raccourci si elle existe et n'est pas déjà dans le HTML
        image_url = shortcut.get('image_url')
        if image_url and image_url not in html_text:
            local_image_path = self.download_image(image_url)
            if local_image_path:
                # Ajouter un marqueur à la fin pour l'image du raccourci
                marker = f"__IMAGE_MARKER_{len(image_positions)}__"
                processed_html += marker
                image_positions.append((len(processed_html), local_image_path))
        
        # Nettoyer le HTML en préservant la mise en forme
        cleaned_html = self.clean_html_preserve_formatting(processed_html)
        
        # Utiliser le HTML nettoyé pour la conversion en texte
        processed_html = cleaned_html
        
        # Convertir le HTML en texte formaté avec html2text
        h = html2text.HTML2Text()
        h.ignore_links = False
        h.ignore_images = True
        h.body_width = 0
        h.unicode_snob = True
        # Essayer de configurer pour des sauts de ligne simples (si l'option existe)
        try:
            h.single_line_break = True
        except AttributeError:
            # L'option n'existe pas dans cette version, on post-traitera après
            pass
        
        # Convertir
        formatted_text = h.handle(processed_html)
        
        # Trouver les positions des marqueurs dans le texte converti
        image_positions_in_text = []
        marker_pattern = r'__IMAGE_MARKER_(\d+)__'
        
        # Trouver tous les marqueurs avec leurs positions (en ordre croissant)
        markers_found = []
        for marker in re.finditer(marker_pattern, formatted_text):
            marker_num = int(marker.group(1))
            if marker_num < len(image_positions):
                position = marker.start()
                image_path = image_positions[marker_num][1]
                marker_length = len(marker.group(0))
                markers_found.append((position, image_path, marker_length))
        
        # Supprimer les marqueurs du texte en ordre inverse pour préserver les positions
        markers_found.sort(key=lambda x: x[0], reverse=True)
        offset = 0
        for position, image_path, marker_length in markers_found:
            # Position ajustée après suppression des marqueurs précédents
            adjusted_position = position - offset
            image_positions_in_text.append((adjusted_position, image_path))
            # Supprimer le marqueur
            formatted_text = formatted_text[:position] + formatted_text[position + marker_length:]
            offset += marker_length
        
        # Trier par position (du début à la fin)
        image_positions_in_text.sort(key=lambda x: x[0])
        
        # Normaliser les sauts de ligne pour éviter les triples et plus, mais garder les simples et doubles
        try:
            if formatted_text:
                # Remplacer tous les types de sauts de ligne par \n
                formatted_text = formatted_text.replace('\r\n', '\n').replace('\r', '\n')
                # Réduire les sauts de ligne multiples (3+ sauts de ligne) à 2 sauts de ligne maximum
                # Cela préserve les sauts de ligne simples (\n) et doubles (\n\n) pour la structure
                formatted_text = re.sub(r'\n{3,}', '\n\n', formatted_text)
                # Réduire les doubles sauts de ligne consécutifs entre paragraphes en simples
                # Mais garder les doubles sauts de ligne qui séparent vraiment des sections
                # On remplace \n\n par \n seulement s'il y a beaucoup de \n\n consécutifs
                # (on garde les doubles sauts de ligne normaux entre paragraphes)
                # En fait, on garde les doubles sauts de ligne car ils séparent bien les paragraphes
        except Exception as e:
            print(f"[MyMind Debug] Erreur lors de la normalisation des sauts de ligne: {e}")
            # En cas d'erreur, retourner le texte tel quel
        
        return formatted_text.strip() if formatted_text else "", cleaned_html, image_positions_in_text
    
    def strip_html(self, html_text: str) -> str:
        """Extrait le texte brut d'un HTML (méthode simple de fallback)"""
        # Simple extraction de texte (pour les champs texte simples)
        # Les champs texte ne supportent généralement pas le HTML
        import re
        text = re.sub(r'<[^>]+>', '', html_text)
        # Décoder les entités HTML simples
        text = text.replace('&nbsp;', ' ')
        text = text.replace('&lt;', '<')
        text = text.replace('&gt;', '>')
        text = text.replace('&amp;', '&')
        return text.strip()
    
    def replace_shortcut(self, shortcut: Dict, text_before_keyword: str):
        """Remplace le raccourci par le texte correspondant"""
        if self.is_replacing:
            print("[MyMind Debug] Remplacement deja en cours, ignore")
            return
        
        self.is_replacing = True
        
        try:
            import pyperclip
            
            # Récupérer le texte original du raccourci
            original_text = shortcut.get('text', '')
            keyword = shortcut.get('keyword', '')
            
            print(f"[MyMind Debug] === DEBUT REMPLACEMENT ===")
            print(f"[MyMind Debug] Mot-cle: '{keyword}'")
            print(f"[MyMind Debug] Texte original (longueur: {len(original_text)}): '{original_text[:100]}...'")
            
            # Remplacer les champs de formulaire AVANT les commandes spéciales
            # (pour que l'utilisateur puisse remplir les champs avant que le texte ne soit traité)
            processed_text = self.replace_form_fields(original_text, shortcut)
            if processed_text is None:
                print("[MyMind Debug] L'utilisateur a annulé le remplissage du formulaire, arrêt du remplacement")
                return  # L'utilisateur a annulé
            
            # Traiter le texte du raccourci (commandes spéciales)
            processed_text, cursor_position = self.replace_special_commands(processed_text)
            print(f"[MyMind Debug] Apres commandes speciales (longueur: {len(processed_text)}): '{processed_text[:100]}...'")
            if cursor_position is not None:
                print(f"[MyMind Debug] Position curseur detectee: {cursor_position}")
            
            # Pour les applications desktop, convertir le HTML en texte formaté
            # et télécharger les images localement
            had_html = self.contains_html(processed_text)
            has_image = shortcut.get('image_url') is not None
            image_positions = []
            formatted_html = None  # HTML avec mise en forme préservée
            
            if had_html or has_image:
                print(f"[MyMind Debug] Texte contient du HTML ou une image, conversion en texte formate...")
                processed_text, formatted_html, image_positions = self.process_html_with_images(processed_text, shortcut)
                print(f"[MyMind Debug] Apres conversion HTML/Image (longueur texte: {len(processed_text)}, longueur HTML: {len(formatted_html) if formatted_html else 0})")
                print(f"[MyMind Debug] {len(image_positions)} image(s) a inserer")
            else:
                # Normaliser les sauts de ligne même pour le texte sans HTML
                try:
                    if processed_text:
                        # Remplacer tous les types de sauts de ligne par \n
                        processed_text = processed_text.replace('\r\n', '\n').replace('\r', '\n')
                        # Réduire les sauts de ligne multiples (3+ sauts de ligne) à 2 sauts de ligne maximum
                        # Cela préserve les sauts de ligne simples (\n) et doubles (\n\n) pour la structure
                        processed_text = re.sub(r'\n{3,}', '\n\n', processed_text)
                except Exception as e:
                    print(f"[MyMind Debug] Erreur lors de la normalisation des sauts de ligne (texte sans HTML): {e}")
                    # En cas d'erreur, continuer avec le texte original
            
            print(f"[MyMind Debug] Texte final a inserer (longueur: {len(processed_text)}): '{processed_text}'")
            
            # Calculer le nombre de caractères à supprimer (le mot-clé)
            chars_to_delete = len(keyword)
            print(f"[MyMind Debug] Nombre de caracteres a supprimer: {chars_to_delete}")
            
            # Sauvegarder le presse-papiers actuel
            old_clipboard = pyperclip.paste()
            print(f"[MyMind Debug] Presse-papiers actuel sauvegarde (longueur: {len(old_clipboard)}): '{old_clipboard[:50]}...'")
            
            # S'assurer que le curseur est à la fin (après avoir récupéré le texte)
            # Appuyer sur End pour aller à la fin de la ligne
            print(f"[MyMind Debug] Deplacement du curseur a la fin...")
            keyboard.press_and_release('end')
            time.sleep(0.1)
            
            # Supprimer le mot-clé en appuyant sur Backspace
            print(f"[MyMind Debug] Suppression de {chars_to_delete} caracteres (mot-cle)...")
            for i in range(chars_to_delete):
                keyboard.press_and_release('backspace')
                time.sleep(0.03)  # Délai un peu plus long pour la stabilité
            print(f"[MyMind Debug] Suppression terminee")
            
            # Si on a des images, insérer le texte par parties avec les images
            if image_positions:
                print(f"[MyMind Debug] Insertion du texte avec images...")
                last_pos = 0
                for img_pos, img_path in image_positions:
                    # Insérer le texte jusqu'à la position de l'image
                    if img_pos > last_pos:
                        text_part = processed_text[last_pos:img_pos]
                        if text_part:
                            pyperclip.copy(text_part)
                            time.sleep(0.1)
                            keyboard.press_and_release('ctrl+v')
                            time.sleep(0.2)
                    
                    # Copier et coller l'image
                    print(f"[MyMind Debug] Insertion de l'image: {img_path}")
                    if self.copy_image_to_clipboard(img_path):
                        time.sleep(0.2)
                        keyboard.press_and_release('ctrl+v')
                        time.sleep(0.3)  # Attendre que l'image soit insérée
                    else:
                        print(f"[MyMind Debug] ATTENTION: Impossible de copier l'image, insertion ignoree")
                        # Ajouter une note textuelle à la place
                        note = f"\n[Image: {os.path.basename(img_path)}]"
                        pyperclip.copy(note)
                        time.sleep(0.1)
                        keyboard.press_and_release('ctrl+v')
                        time.sleep(0.2)
                    
                    last_pos = img_pos
                
                # Insérer le reste du texte
                if last_pos < len(processed_text):
                    text_part = processed_text[last_pos:]
                    if text_part:
                        pyperclip.copy(text_part)
                        time.sleep(0.1)
                        keyboard.press_and_release('ctrl+v')
                        time.sleep(0.2)
            else:
                # Pas d'images, insertion du texte ou du HTML formaté
                if formatted_html and had_html:
                    # Utiliser le HTML formaté pour préserver la mise en forme
                    # Le HTML est copié avec le texte brut pour compatibilité (Notepad, etc.)
                    print(f"[MyMind Debug] Copie du HTML formate dans le presse-papiers (preservation de la mise en forme)...")
                    if self.copy_html_to_clipboard(formatted_html, processed_text):
                        time.sleep(0.15)  # Attendre que le presse-papiers soit mis à jour
                        # Insérer avec Ctrl+V (l'application choisira le format qu'elle supporte)
                        print(f"[MyMind Debug] Insertion avec Ctrl+V (HTML pour Word/Outlook, texte brut pour Notepad)...")
                        keyboard.press_and_release('ctrl+v')
                        time.sleep(0.3)  # Attendre que le texte soit inséré
                    else:
                        # Fallback sur le texte brut si la copie HTML échoue
                        print(f"[MyMind Debug] Echec de la copie HTML, utilisation du texte brut...")
                        pyperclip.copy(processed_text)
                        time.sleep(0.15)
                        keyboard.press_and_release('ctrl+v')
                        time.sleep(0.3)
                else:
                    # Pas de HTML, insertion normale du texte
                    print(f"[MyMind Debug] Copie du texte dans le presse-papiers...")
                    print(f"[MyMind Debug] Texte a copier: '{processed_text}'")
                    pyperclip.copy(processed_text)
                    time.sleep(0.15)  # Attendre que le presse-papiers soit mis à jour
                    
                    # Insérer le texte avec Ctrl+V (plus fiable que keyboard.write())
                    print(f"[MyMind Debug] Insertion du texte avec Ctrl+V...")
                    keyboard.press_and_release('ctrl+v')
                    time.sleep(0.3)  # Attendre que le texte soit inséré
            
            print(f"[MyMind Debug] Insertion terminee")
            
            # Positionner le curseur si nécessaire
            if cursor_position is not None:
                print(f"[MyMind Debug] Positionnement du curseur a la position {cursor_position}...")
                try:
                    # La position du curseur est relative au texte original (avant conversion HTML)
                    # Après l'insertion, le curseur est à la fin du texte inséré
                    # Il faut donc calculer combien de caractères il faut reculer depuis la fin
                    
                    # Le processed_text final est le texte qui a été réellement inséré
                    # (après conversion HTML en texte brut)
                    text_length = len(processed_text)
                    
                    # Ajuster la position si nécessaire (si cursor_position dépasse la longueur finale)
                    if cursor_position > text_length:
                        cursor_position = text_length
                        print(f"[MyMind Debug] Position curseur ajustee a {cursor_position} (limite du texte)")
                    
                    if cursor_position >= 0:
                        # Calculer combien de caractères il faut reculer depuis la fin
                        chars_to_move_back = text_length - cursor_position
                        print(f"[MyMind Debug] Deplacement du curseur: {chars_to_move_back} caracteres vers la gauche (position {cursor_position}/{text_length})")
                        
                        if chars_to_move_back == 0:
                            print(f"[MyMind Debug] Curseur deja a la bonne position (fin du texte)")
                        elif chars_to_move_back <= 200:
                            # Pour les distances raisonnables, utiliser les flèches gauche
                            # Petit délai pour s'assurer que l'insertion est terminée
                            time.sleep(0.2)
                            for _ in range(chars_to_move_back):
                                keyboard.press_and_release('left')
                                time.sleep(0.005)  # Petit délai pour la stabilité
                            print(f"[MyMind Debug] Curseur positionne avec succes (methode fleches)")
                        else:
                            # Pour les grandes distances, utiliser une méthode plus efficace
                            # Aller au début puis avancer jusqu'à la position
                            print(f"[MyMind Debug] Distance trop grande, utilisation de Home puis fleches droite")
                            time.sleep(0.2)
                            keyboard.press_and_release('home')
                            time.sleep(0.1)
                            
                            # Avancer jusqu'à la position (mais limiter à 200 caractères pour éviter les délais)
                            if cursor_position <= 200:
                                for _ in range(cursor_position):
                                    keyboard.press_and_release('right')
                                    time.sleep(0.005)
                                print(f"[MyMind Debug] Curseur positionne depuis le debut de la ligne")
                            else:
                                # Si c'est vraiment trop long, utiliser Ctrl+Right pour naviguer par mots
                                # Puis finir avec les flèches
                                words_to_skip = cursor_position // 10  # Approximatif
                                for _ in range(min(words_to_skip, 50)):  # Limiter à 50 mots
                                    keyboard.press_and_release('ctrl+right')
                                    time.sleep(0.01)
                                # Finir avec les flèches droite pour la précision
                                remaining = cursor_position - (words_to_skip * 10)
                                for _ in range(min(remaining, 50)):
                                    keyboard.press_and_release('right')
                                    time.sleep(0.005)
                                print(f"[MyMind Debug] Curseur positionne approximativement (methode mots)")
                    else:
                        print(f"[MyMind Debug] Position curseur invalide: {cursor_position}")
                except Exception as e:
                    print(f"[MyMind Debug] Erreur lors du positionnement du curseur: {e}")
                    import traceback
                    traceback.print_exc()
            
            # Restaurer le presse-papiers
            try:
                pyperclip.copy(old_clipboard)
                print(f"[MyMind Debug] Presse-papiers restaure")
            except Exception as e:
                print(f"[MyMind Debug] Erreur lors de la restauration du presse-papiers: {e}")
            
            print(f"[MyMind Debug] === FIN REMPLACEMENT ===")
            
            if self.on_shortcut_found:
                self.on_shortcut_found(shortcut)
        
        except Exception as e:
            print(f"[MyMind Debug] ERREUR lors du remplacement du raccourci: {e}")
            import traceback
            traceback.print_exc()
        finally:
            time.sleep(0.1)
            self.is_replacing = False
            print(f"[MyMind Debug] Flag is_replacing remis a False")
    
    def contains_html(self, text: str) -> bool:
        """Vérifie si le texte contient du HTML"""
        if not text:
            return False
        return bool(re.search(r'<[^>]+>', text))
    
    def get_current_text_from_field(self) -> str:
        """Récupère le texte actuel du champ de saisie actif"""
        try:
            import pyperclip
            print(f"[MyMind Debug] === DEBUT RECUPERATION TEXTE ===")
            
            # Sauvegarder le presse-papiers actuel
            old_clipboard = pyperclip.paste()
            print(f"[MyMind Debug] Presse-papiers initial: '{old_clipboard[:50]}...' (longueur: {len(old_clipboard)})")
            
            # Méthode améliorée : utiliser plusieurs tentatives
            current_text = ""
            
            # Tentative 1: Sélectionner depuis le curseur jusqu'au début de la ligne
            print(f"[MyMind Debug] Tentative 1: Shift+Home (depuis curseur jusqu'au debut de ligne)...")
            try:
                keyboard.press_and_release('shift+home')
                time.sleep(0.2)
                keyboard.press_and_release('ctrl+c')
                time.sleep(0.3)  # Délai plus long
                current_text = pyperclip.paste()
                print(f"[MyMind Debug] Tentative 1 resultat: '{current_text[:100]}...' (longueur: {len(current_text)})")
                
                # Si on a récupéré du texte, l'utiliser
                if current_text and len(current_text.strip()) > 0:
                    print(f"[MyMind Debug] Tentative 1 reussie, utilisation de ce texte")
                    # Restaurer le presse-papiers et retourner
                    try:
                        pyperclip.copy(old_clipboard)
                    except:
                        pass
                    print(f"[MyMind Debug] === FIN RECUPERATION TEXTE (tentative 1) ===")
                    return current_text
                
                # Annuler la sélection
                print(f"[MyMind Debug] Tentative 1 echouee, annulation de la selection...")
                keyboard.press_and_release('right')
                time.sleep(0.1)
            except Exception as e:
                print(f"[MyMind Debug] Erreur tentative 1: {e}")
            
            # Tentative 2: Sélectionner tout le champ (Ctrl+A)
            print(f"[MyMind Debug] Tentative 2: Ctrl+A (selectionner tout)...")
            try:
                keyboard.press_and_release('ctrl+a')
                time.sleep(0.2)
                keyboard.press_and_release('ctrl+c')
                time.sleep(0.3)
                current_text = pyperclip.paste()
                print(f"[MyMind Debug] Tentative 2 resultat: '{current_text[:100]}...' (longueur: {len(current_text)})")
                
                if current_text and len(current_text.strip()) > 0:
                    print(f"[MyMind Debug] Tentative 2 reussie, utilisation de ce texte")
                    # Restaurer le presse-papiers
                    try:
                        pyperclip.copy(old_clipboard)
                    except:
                        pass
                    print(f"[MyMind Debug] === FIN RECUPERATION TEXTE (tentative 2) ===")
                    return current_text
                else:
                    print(f"[MyMind Debug] Tentative 2 echouee (texte vide)")
            except Exception as e:
                print(f"[MyMind Debug] Erreur tentative 2: {e}")
            
            # Si aucune méthode n'a fonctionné, restaurer le presse-papiers
            print(f"[MyMind Debug] Aucune tentative n'a reussi, retour texte vide")
            try:
                pyperclip.copy(old_clipboard)
            except:
                pass
            
            print(f"[MyMind Debug] === FIN RECUPERATION TEXTE (echec) ===")
            return current_text or ""
        except Exception as e:
            print(f"[MyMind Debug] ERREUR lors de la recuperation du texte: {e}")
            import traceback
            traceback.print_exc()
            print(f"[MyMind Debug] === FIN RECUPERATION TEXTE (exception) ===")
            return ""
    
    def clear_buffer(self):
        """Vide le buffer de texte"""
        self.text_buffer = ""
        self.is_capturing = False
        self.buffer_start_time = 0
    
    def key_to_char(self, key_name: str, is_shift: bool = False) -> Optional[str]:
        """Convertit un nom de touche en caractère"""
        # Mapping des touches aux caractères
        key_map = {
            'space': ' ',
            'enter': '\n',
            'tab': '\t',
            'slash': '/',
            '/': '/',
            'backslash': '\\',
            'minus': '-',
            '-': '-',
            'equals': '=',
            '=': '=',
            'comma': ',',
            ',': ',',
            'period': '.',
            '.': '.',
            'semicolon': ';',
            ';': ';',
            'quote': "'",
            "'": "'",
            'left bracket': '[',
            '[': '[',
            'right bracket': ']',
            ']': ']',
        }
        
        # Si c'est une touche spéciale, retourner le caractère correspondant
        if key_name in key_map:
            return key_map[key_name]
        
        # Si c'est une lettre
        if len(key_name) == 1 and key_name.isalpha():
            return key_name.upper() if is_shift else key_name.lower()
        
        # Si c'est un chiffre
        if key_name.startswith('digit ') or key_name in [str(i) for i in range(10)]:
            if key_name.startswith('digit '):
                num = key_name.replace('digit ', '')
            else:
                num = key_name
            if is_shift:
                # Chiffres avec Shift
                shift_map = {'1': '!', '2': '@', '3': '#', '4': '$', '5': '%', 
                           '6': '^', '7': '&', '8': '*', '9': '(', '0': ')'}
                return shift_map.get(num, num)
            return num
        
        return None
    
    def get_shortcut_prefixes(self) -> set:
        """Récupère tous les premiers caractères des raccourcis"""
        prefixes = set()
        for shortcut in self.shortcuts_cache:
            keyword = shortcut.get('keyword', '')
            if keyword and len(keyword) > 0:
                prefixes.add(keyword[0].lower())
        return prefixes
    
    def on_key_press(self, event):
        """Gère les frappes de touches"""
        if not self.is_enabled or self.is_replacing:
            return
        
        key_name = event.name
        
        # Gérer le backspace pendant la capture
        if key_name == 'backspace' and self.is_capturing:
            if len(self.text_buffer) > 0:
                self.text_buffer = self.text_buffer[:-1]
                print(f"[MyMind Debug] Backspace, buffer: '{self.text_buffer}'")
                if len(self.text_buffer) == 0:
                    self.clear_buffer()
            # Laisser le backspace s'exécuter normalement
            return
        
        # Ignorer les touches spéciales (sauf celles qui déclenchent le remplacement)
        modifier_keys = ['ctrl', 'alt', 'shift', 'cmd', 'windows', 'left', 'right', 'up', 'down', 'home', 'end', 'page up', 'page down']
        if key_name in modifier_keys:
            # Si on est en train de capturer et qu'on appuie sur une touche de navigation, arrêter la capture
            if self.is_capturing and key_name in ['left', 'right', 'up', 'down', 'home', 'end']:
                self.clear_buffer()
            return
        
        # Détecter si Shift est pressé (pour les majuscules)
        is_shift = keyboard.is_pressed('shift')
        
        # Convertir la touche en caractère
        char = self.key_to_char(key_name, is_shift)
        
        # Récupérer les préfixes de raccourcis (premiers caractères)
        shortcut_prefixes = self.get_shortcut_prefixes()
        
        # Si on n'est pas en train de capturer et qu'on tape un préfixe de raccourci, commencer la capture
        if not self.is_capturing and char and char.lower() in shortcut_prefixes:
            print(f"[MyMind Debug] Prefixe detecte: '{char}', debut de la capture")
            self.is_capturing = True
            self.text_buffer = char
            self.buffer_start_time = time.time()
            return  # Laisser le caractère s'afficher normalement
        
        # Si on est en train de capturer
        if self.is_capturing:
            # Vérifier si c'est une touche de déclenchement
            trigger_keys = self.config.get('trigger_keys', ['space', 'enter', 'tab'])
            is_trigger = key_name in trigger_keys
            
            if is_trigger:
                # Vérifier si le buffer correspond à un raccourci
                print(f"[MyMind Debug] Touche de declenchement detectee, buffer: '{self.text_buffer}'")
                
                # Trouver le raccourci correspondant au buffer
                shortcut = None
                for s in self.shortcuts_cache:
                    if s.get('keyword', '').lower() == self.text_buffer.lower():
                        shortcut = s
                        break
                
                if shortcut:
                    print(f"[MyMind Debug] ===== RACCOURCI TROUVE: '{shortcut.get('keyword')}' =====")
                    
                    # Supprimer la touche de déclenchement qui vient d'être ajoutée
                    if key_name in ['space', 'tab']:
                        keyboard.press_and_release('backspace')
                        time.sleep(0.05)
                    
                    # Supprimer le mot-clé qui a été tapé (en utilisant backspace)
                    keyword_length = len(self.text_buffer)
                    for _ in range(keyword_length):
                        keyboard.press_and_release('backspace')
                        time.sleep(0.02)
                    
                    # Remplacer le raccourci
                    self.replace_shortcut(shortcut, "")
                    
                    # Si c'était un espace ou tab, l'ajouter après
                    if key_name in ['space', 'tab']:
                        time.sleep(0.1)
                        import pyperclip
                        old_clip = pyperclip.paste()
                        pyperclip.copy(' ')
                        keyboard.press_and_release('ctrl+v')
                        time.sleep(0.05)
                        try:
                            pyperclip.copy(old_clip)
                        except:
                            pass
                    
                    # Vider le buffer
                    self.clear_buffer()
                    return  # Empêcher la touche de déclenchement de s'exécuter
                else:
                    # Pas de raccourci trouvé, vider le buffer et laisser la touche s'exécuter
                    print(f"[MyMind Debug] Aucun raccourci trouve pour '{self.text_buffer}', vider le buffer")
                    self.clear_buffer()
                    return  # Laisser la touche de déclenchement s'exécuter normalement
            
            # Si ce n'est pas une touche de déclenchement, ajouter le caractère au buffer
            # On continue à capturer jusqu'à la touche de déclenchement
            if char:
                # Ajouter le caractère au buffer
                self.text_buffer += char
                print(f"[MyMind Debug] Buffer mis a jour: '{self.text_buffer}'")
                # Laisser le caractère s'afficher normalement
                
                # Vérifier si le buffer dépasse la longueur maximale d'un raccourci
                max_keyword_length = max([len(s.get('keyword', '')) for s in self.shortcuts_cache] + [50])
                if len(self.text_buffer) > max_keyword_length:
                    print(f"[MyMind Debug] Buffer trop long, vider le buffer")
                    self.clear_buffer()
            else:
                # Caractère non reconnu ou touche spéciale, vider le buffer seulement si ce n'est pas une touche de navigation
                if key_name not in ['left', 'right', 'up', 'down', 'home', 'end']:
                    print(f"[MyMind Debug] Caractere non reconnu ou touche speciale, vider le buffer")
                    self.clear_buffer()
        
        # Si on n'est pas en train de capturer et que ce n'est pas un préfixe, ne rien faire
        # (le caractère s'affichera normalement)
    
    def start(self):
        """Démarre l'écoute des raccourcis"""
        # Enregistrer le hook global avec suppression optionnelle
        # Note: keyboard.on_press ne supporte pas suppress directement
        # On gère le blocage manuellement dans on_key_press
        keyboard.on_press(self.on_key_press)
        print("[MyMind] Gestionnaire de raccourcis demarre")
    
    def stop(self):
        """Arrête l'écoute des raccourcis"""
        keyboard.unhook_all()
        print("[MyMind] Gestionnaire de raccourcis arrêté")

