using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using UnityEngine.InputSystem;
using UnityEngine.Networking;
using TMPro;
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;

#if UNITY_WEBGL && !UNITY_EDITOR
using System.Runtime.InteropServices;
#endif

public class SubtitleManager : MonoBehaviour
{
    // ─────────────────────────────────────────────────────────────────────────────
    // Données JSON
    // ─────────────────────────────────────────────────────────────────────────────
    
    // AJOUT : Callback pour la fin du dialogue
    public System.Action OnDialogueComplete { get; set; }

    [Serializable] internal class SubLine { public string text; public string image; }
    [Serializable] internal class Segment
    {
        public string id;       // identifiant unique (pour le menu / ?seg=)
        public string title;    // titre à afficher dans le menu
        public string video;    // nom de fichier ou URL
        public string speaker;  // nom de l'intervenant
        public List<SubLine> lines;
    }
    [Serializable] internal class Project
    {
        public string videoRoot;   // ex: https://ton-domaine/videos/
        public string imageRoot;   // ex: https://ton-domaine/assets/images/
        public List<Segment> segments;
    }

    // Structure pour parser le JSON de dialogue
    [Serializable]
    private class DialogueWrapper
    {
        public DialogueData dialogue;
    }

    [Serializable] 
    private class DialogueData
    {
        public string title;
        public string speaker;
        public string video;
        public List<DialogueLine> lines;
    }

    [Serializable]
    private class DialogueLine
    {
        public string text;
        public string image;
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Références UI (Editor/Desktop uniquement)
    // ─────────────────────────────────────────────────────────────────────────────
    [Header("UI (Unity - Desktop/Editor)")]
    public TMP_Text speakerText;
    public TMP_Text subtitleText;

    [Header("Overlay Image (Unity - Desktop/Editor)")]
    public Image overlayImage;
    [Range(0f, 2f)] public float overlayFadeInDuration = 0.25f;

    // Container + mask rond (Desktop/Editor) pour appliquer bordure + fond + forme circulaire
    private GameObject overlayImageContainer;
    private CanvasGroup overlayImageCanvasGroup;
    private RectTransform overlayInnerMaskRect;
    private CircleMaskGraphic overlayBorderGraphic;
    private CircleMaskGraphic overlayMaskGraphic;
    private float overlayBorderWidthPx = 4f;

    [Header("Video Surface (Desktop/Editor)")]
    public RawImage videoSurface;
    [Range(0f, 1f)] public float videoFadeInSeconds = 0.25f;

    [Header("Video Scale")]
    [Tooltip("true = Fit (barres noires), false = Fill (crop)")]
    public bool letterbox = false;
    private AspectRatioFitter videoAspect;

    [Header("FX (optionnel, Desktop/Editor)")]
    public ParticleSystem sparklePrefab;
    public Camera fxCamera;
    public float fxSpawnDistance = 1f;
    public float fxGlowExpand = 1.08f;

    // ─────────────────────────────────────────────────────────────────────────────
    // Vidéo (Desktop/Editor)
    // ─────────────────────────────────────────────────────────────────────────────
    [Header("Video (Desktop/Editor)")]
    public VideoPlayer videoPlayer;
    public bool loopVideoEachSegment = true;

    // ─────────────────────────────────────────────────────────────────────────────
    // Données & Contrôles
    // ─────────────────────────────────────────────────────────────────────────────
    [Header("Data")]
    public string jsonFileName = "subtitles.json";  // placé dans Assets/StreamingAssets/
    public bool loopAllSegments = false;

    [Header("Segment pick (menu)")]
    public bool restrictToSelectedSegment = true;  // rester dans le segment choisi
    public string defaultSegmentId = "";           // fallback si aucun choix n'a été passé
    private int lockedSegmentIndex = -1;           // si >=0 on ne sort pas de ce segment

    [Header("Controls (Input System)")]
    public Key nextKey = Key.Space;
    public Key prevKey = Key.Backspace;

    // ─────────────────────────────────────────────────────────────────────────────
    // État interne
    // ─────────────────────────────────────────────────────────────────────────────
    private Project project;
    private int segIndex, lineIndex;

    private string lastOverlayImageName = null;
    private Coroutine overlayFadeCoroutine = null;

    private Coroutine prepareCoroutine = null;
    private int videoLoadSeq = 0;

    // Tracker anti-race + handles de coroutines (évite crash quand une coroutine finit après un retour scène)
    private int _token = 0;
    private Coroutine _loadDialogueCoroutine = null;
    private Coroutine _overlayLoadCoroutine = null;
    private bool _cleanupRegistered = false;

    private RenderTexture videoRT;
    private readonly Dictionary<string, Sprite> spriteCache = new Dictionary<string, Sprite>();

    private bool IsWebGL => Application.platform == RuntimePlatform.WebGLPlayer;

#if UNITY_WEBGL && !UNITY_EDITOR
    // ─────────────────────────────────────────────────────────────────────────────
    // Pont .jslib (WebGL)
    // ─────────────────────────────────────────────────────────────────────────────
    [DllImport("__Internal")] private static extern void WVO_Create(string url, string fit, int loop, int muted);
    [DllImport("__Internal")] private static extern void WVO_Play();
    [DllImport("__Internal")] private static extern void WVO_Dispose();
    [DllImport("__Internal")] private static extern void WVO_SetObjectFit(string fit);

    [DllImport("__Internal")] private static extern void UI_SetSpeaker(string s);
    [DllImport("__Internal")] private static extern void UI_SetSubtitle(string s);
    [DllImport("__Internal")] private static extern void UI_SetSpeakerHtml(string s);
    [DllImport("__Internal")] private static extern void UI_SetSubtitleHtml(string s);
    [DllImport("__Internal")] private static extern void UI_SetVisible(int visible);

    [DllImport("__Internal")] private static extern void UI_ShowOverlayImage(string url, int fadeMs, int skipFadeIfSame);
    [DllImport("__Internal")] private static extern void UI_HideOverlayImage(int fadeMs);
    [DllImport("__Internal")] private static extern void UI_SetOverlayImagePosition(float x, float y);
    [DllImport("__Internal")] private static extern void UI_SetOverlayImageStyle(float w, float h, float borderW, string borderColor, string bgColor, string gradStart, string gradEnd);
    
    [DllImport("__Internal")] private static extern void UI_SetSubtitleStyle(string bgColor, float bgAlpha, string speakerColor, string dialogueColor, string dialogueSize, string speakerSize);
#endif

    // ─────────────────────────────────────────────────────────────────────────────
    // Cycle de vie
    // ─────────────────────────────────────────────────────────────────────────────
    private void Awake()
    {
        if (speakerText)  speakerText.text  = "";
        if (subtitleText) subtitleText.text = "";

        if (overlayImage)
        {
            overlayImage.sprite = null;
            var c = overlayImage.color; c.a = 1f; overlayImage.color = c;
            overlayImage.preserveAspect = true;
            SetupOverlayImageCircleContainer();
        }

        if (videoSurface)
        {
            var vc = videoSurface.color; vc.a = 0f; videoSurface.color = vc;
            videoAspect = videoSurface.GetComponent<AspectRatioFitter>() ?? videoSurface.gameObject.AddComponent<AspectRatioFitter>();

            if (videoSurface.texture == null)
            {
                var tex = new Texture2D(2, 2, TextureFormat.RGBA32, false);
                tex.SetPixels(new[] { Color.black, Color.black, Color.black, Color.black });
                tex.Apply();
                videoSurface.texture = tex;
                vc.a = 1f; videoSurface.color = vc;
            }
        }

        // Modèle "cleanup point" : au changement de scène (ex: retour Map), couper tout ce qui peut rester en vol
        // (coroutines, overlays DOM, RT vidéo) pour éviter les crashs WebGL.
        try
        {
            if (!_cleanupRegistered && CleanupPoints.Instance != null)
            {
                _cleanupRegistered = true;
                CleanupPoints.Instance.RegisterOnSceneChange("SubtitleManager", SafeCleanupForSceneChange);
            }
        }
        catch { /* ignore */ }
    }

    private void OnDisable()
    {
        // OnDisable peut arriver pendant des transitions: on fait un cleanup dur pour éviter les restes.
        ForceCleanupAndHide();
#if UNITY_WEBGL && !UNITY_EDITOR
        try { UI_SetVisible(0); } catch {}
        try { UI_HideOverlayImage((int)(overlayFadeInDuration * 1000f)); } catch {}
        try { WVO_Dispose(); } catch {}
#endif
    }
    
    private void OnDestroy()
    {
        try
        {
            var cp = CleanupPoints.GetExisting();
            if (cp != null) cp.UnregisterOnSceneChange("SubtitleManager");
        }
        catch { /* ignore */ }

        // 🧹 IMPORTANT : Nettoyer le DialogueBottomBanner quand le SubtitleManager est détruit
        // Sinon il persiste sur la Map après le retour depuis un jeu
        Debug.Log("[SubtitleManager] OnDestroy - Nettoyage du DialogueBottomBanner");
        
        GameObject bottomBanner = GameObject.Find("DialogueBottomBanner");
        if (bottomBanner != null)
        {
            Debug.Log("[SubtitleManager] ✅ DialogueBottomBanner trouvé et détruit");
            Destroy(bottomBanner);
        }
        
        // Nettoyer aussi tous les éléments UI créés
        ForceCleanupAndHide();
    }

    private void SafeCleanupForSceneChange()
    {
        ForceCleanupAndHide();
    }

    public void ForceCleanupAndHide()
    {
        // Invalider tout ce qui est en vol
        _token++;

        // Libérer le cache d'images (sprites + textures téléchargées)
        try
        {
            foreach (var kv in spriteCache)
            {
                var sp = kv.Value;
                if (sp == null) continue;
                Texture tex = null;
                try { tex = sp.texture; } catch { }
                Destroy(sp);
                if (tex != null) Destroy(tex);
            }
            spriteCache.Clear();
        }
        catch { /* ignore */ }

        // Stopper les coroutines du manager (downloads JSON/images, fades, prepare video, etc.)
        try { StopAllCoroutines(); } catch { /* ignore */ }
        prepareCoroutine = null;
        overlayFadeCoroutine = null;
        _loadDialogueCoroutine = null;
        _overlayLoadCoroutine = null;

        // Stop vidéo desktop
        try { if (videoPlayer != null) videoPlayer.Stop(); } catch { /* ignore */ }

        // Nettoyage RT desktop
        CleanupVideoRT();

        // Nettoyage WebGL DOM (vidéo + overlay image + bandeau)
        CleanupWebGLVideo();

        // Hide UI
        try { ForceHideAllUI(); } catch { /* ignore */ }
    }

    private IEnumerator Start()
    {
        // Mode URL dynamique vs mode fichier local
        bool useUrlMode = string.IsNullOrEmpty(jsonFileName);
        
        if (!useUrlMode)
        {
            // Mode ancien : charger subtitles.json
            yield return StartCoroutine(LoadJson());
            if (project == null || project.segments == null || project.segments.Count == 0)
            {
                Debug.LogError("[SubtitleManager] Aucun segment trouvé dans le JSON.");
                yield break;
            }
            
            SetupVideoPlayerAndSegments();
        }
        else
        {
            // Mode URL - Attendre qu'on nous passe une URL de dialogue
            Debug.Log("[SubtitleManager] Mode URL - Attente d'un dialogue depuis GameConfigLoader");
            SetupBasicVideoPlayer();
            yield break;
        }
    }

    private void SetupVideoPlayerAndSegments()
    {
        if (!IsWebGL)
        {
            if (!videoPlayer)
            {
                videoPlayer = FindFirstObjectByType<VideoPlayer>();
                if (!videoPlayer) { Debug.LogError("[SubtitleManager] Aucun VideoPlayer dans la scène."); return; }
            }
            videoPlayer.playOnAwake = false;
            videoPlayer.isLooping = loopVideoEachSegment;
        }

        if (videoSurface) videoSurface.rectTransform.SetAsFirstSibling();

#if UNITY_WEBGL && !UNITY_EDITOR
        try { UI_SetVisible(1); } catch {}
#endif

        string wantedId = SegmentSelector.SelectedSegmentId;

#if UNITY_WEBGL && !UNITY_EDITOR
        if (string.IsNullOrEmpty(wantedId))
        {
            try
            {
                var url = Application.absoluteURL;
                int qi = url.IndexOf('?');
                if (qi >= 0)
                {
                    var qs = url.Substring(qi + 1).Split('&');
                    foreach (var kv in qs)
                    {
                        var p = kv.Split('=');
                        if (p.Length == 2 && p[0] == "seg") { wantedId = WWW.UnEscapeURL(p[1]); break; }
                    }
                }
            }
            catch { }
        }
#endif
        if (string.IsNullOrEmpty(wantedId) && !string.IsNullOrEmpty(defaultSegmentId))
            wantedId = defaultSegmentId;

        if (!string.IsNullOrEmpty(wantedId))
        {
            int ix = project.segments.FindIndex(s => string.Equals(s.id, wantedId, StringComparison.OrdinalIgnoreCase));
            if (ix >= 0)
            {
                segIndex = ix;
                if (restrictToSelectedSegment) lockedSegmentIndex = ix;
            }
        }

        lineIndex = 0;
        LoadSegment(segIndex);
        ApplyCurrentLine();
    }

    private void SetupBasicVideoPlayer()
    {
        if (!IsWebGL && !videoPlayer)
        {
            videoPlayer = FindFirstObjectByType<VideoPlayer>();
            if (videoPlayer != null)
            {
                videoPlayer.playOnAwake = false;
                videoPlayer.isLooping = loopVideoEachSegment;
            }
        }
        
        if (videoSurface) videoSurface.rectTransform.SetAsFirstSibling();

#if UNITY_WEBGL && !UNITY_EDITOR
        try { UI_SetVisible(1); } catch {}
#endif
    }

    // NOUVELLE MÉTHODE : Pour charger un dialogue depuis URL
    public void LoadDialogueFromUrl(string dialogueUrl)
    {
        Debug.Log($"=== SUBTITLE MANAGER ===");
        Debug.Log($"URL reçue: '{dialogueUrl}'");
        Debug.Log($"OnDialogueComplete callback: {(OnDialogueComplete != null ? "Défini" : "NULL")}");
        
        if (string.IsNullOrEmpty(dialogueUrl))
        {
            Debug.LogError("[SubtitleManager] URL de dialogue vide");
            CleanupWebGLVideo();
            OnDialogueComplete?.Invoke();
            return;
        }
        
        Debug.Log($"[SubtitleManager] Chargement dialogue depuis: {dialogueUrl}");
        _token++;
        if (_loadDialogueCoroutine != null) StopCoroutine(_loadDialogueCoroutine);
        _loadDialogueCoroutine = StartCoroutine(LoadDialogueJson(dialogueUrl));
    }
    
    // NOUVELLE MÉTHODE : Charger depuis un JSON string directement
    public void LoadDialogueFromJson(string jsonString)
    {
        Debug.Log($"=== SUBTITLE MANAGER (JSON String) ===");
        Debug.Log($"OnDialogueComplete callback: {(OnDialogueComplete != null ? "Défini" : "NULL")}");
        
        if (string.IsNullOrEmpty(jsonString))
        {
            Debug.LogError("[SubtitleManager] JSON de dialogue vide");
            CleanupWebGLVideo();
            OnDialogueComplete?.Invoke();
            return;
        }
        
        Debug.Log($"[SubtitleManager] Parsing dialogue depuis JSON string");
        ParseAndLoadDialogue(jsonString);
    }
    
    private void ParseAndLoadDialogue(string json)
    {
        try
        {
            Debug.Log($"[SubtitleManager] Parsing JSON ({json.Length} caractères)");
            
            var dialogueData = JsonUtility.FromJson<DialogueWrapper>(json);
            
            Debug.Log($"[SubtitleManager] dialogueData: {(dialogueData != null ? "OK" : "NULL")}");
            Debug.Log($"[SubtitleManager] dialogue: {(dialogueData?.dialogue != null ? "OK" : "NULL")}");
            Debug.Log($"[SubtitleManager] lines: {(dialogueData?.dialogue?.lines != null ? $"OK ({dialogueData.dialogue.lines.Count})" : "NULL")}");
            
            if (dialogueData?.dialogue?.lines != null && dialogueData.dialogue.lines.Count > 0)
            {
                Debug.Log($"[SubtitleManager] {dialogueData.dialogue.lines.Count} lignes de dialogue trouvées");
                
                var segment = ConvertDialogueToSegment(dialogueData.dialogue);
                
                // Récupérer les chemins depuis general-config.json
                string videoRoot = "";
                string imageRoot = "";
                
                if (GeneralConfigManager.Instance != null)
                {
                    var config = GeneralConfigManager.Instance.GetConfig();
                    if (config?.assetsPaths != null)
                    {
                        videoRoot = config.assetsPaths.videoPath ?? "";
                        imageRoot = config.assetsPaths.decoratorImagePath ?? "";
                    }
                }
                
                // Si GeneralConfigManager n'est pas disponible ou les valeurs sont vides, laisser vide
                // Les scripts devront gérer le cas où les valeurs sont vides
                if (string.IsNullOrEmpty(videoRoot))
                {
                    Debug.LogError("[SubtitleManager] ❌ videoPath non défini dans general-config.json");
                }
                if (string.IsNullOrEmpty(imageRoot))
                {
                    Debug.LogError("[SubtitleManager] ❌ decoratorImagePath non défini dans general-config.json");
                }
                
                project = new Project 
                { 
                    segments = new List<Segment> { segment },
                    videoRoot = videoRoot,
                    imageRoot = imageRoot
                };
                
                segIndex = 0;
                lineIndex = 0;
                dialogueStartTime = Time.time; // Enregistrer le début du dialogue
                
                // Appliquer la config de dialogue si pas déjà fait
                EnsureDialogueConfigApplied();
                
                LoadSegment(segIndex);
                ApplyCurrentLine();
            }
            else
            {
                Debug.LogWarning("[SubtitleManager] Aucune ligne de dialogue trouvée - JSON:");
                Debug.LogWarning(json.Substring(0, Mathf.Min(500, json.Length)));
                CleanupWebGLVideo();
                OnDialogueComplete?.Invoke();
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"[SubtitleManager] Erreur parsing JSON dialogue: {e.Message}");
            Debug.LogError($"Stack trace: {e.StackTrace}");
            Debug.LogError($"JSON reçu: {json}");
            CleanupWebGLVideo();
            OnDialogueComplete?.Invoke();
        }
    }

    private IEnumerator LoadDialogueJson(string url)
    {
        int token = _token;
        using (var www = UnityWebRequest.Get(url))
        {
            yield return www.SendWebRequest();
            if (token != _token) yield break;
            
            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"[SubtitleManager] Erreur chargement dialogue: {www.error}");
                CleanupWebGLVideo();
                OnDialogueComplete?.Invoke();
                yield break;
            }
            
            try
            {
                string json = www.downloadHandler.text;
                Debug.Log($"[SubtitleManager] JSON dialogue reçu");
                
                var dialogueData = JsonUtility.FromJson<DialogueWrapper>(json);
                
                if (dialogueData?.dialogue?.lines != null && dialogueData.dialogue.lines.Count > 0)
                {
                    Debug.Log($"[SubtitleManager] {dialogueData.dialogue.lines.Count} lignes de dialogue trouvées");
                    
                    var segment = ConvertDialogueToSegment(dialogueData.dialogue);
                    
                    // Récupérer les chemins depuis general-config.json
                    string videoRoot = "";
                    string imageRoot = "";
                    
                    if (GeneralConfigManager.Instance != null)
                    {
                        var config = GeneralConfigManager.Instance.GetConfig();
                        if (config?.assetsPaths != null)
                        {
                            videoRoot = config.assetsPaths.videoPath ?? "";
                            imageRoot = config.assetsPaths.decoratorImagePath ?? "";
                        }
                    }
                    
                        // Si GeneralConfigManager n'est pas disponible ou les valeurs sont vides, laisser vide
                        // Les scripts devront gérer le cas où les valeurs sont vides
                        if (string.IsNullOrEmpty(videoRoot))
                        {
                            Debug.LogError("[SubtitleManager] ❌ videoPath non défini dans general-config.json");
                        }
                        if (string.IsNullOrEmpty(imageRoot))
                        {
                            Debug.LogError("[SubtitleManager] ❌ decoratorImagePath non défini dans general-config.json");
                        }
                    
                    project = new Project 
                    { 
                        segments = new List<Segment> { segment },
                        videoRoot = videoRoot,
                        imageRoot = imageRoot
                    };
                    
                    segIndex = 0;
                    lineIndex = 0;
                    dialogueStartTime = Time.time; // Enregistrer le début du dialogue
                    
                    // Appliquer la config de dialogue si pas déjà fait
                    EnsureDialogueConfigApplied();
                    
                    LoadSegment(segIndex);
                    ApplyCurrentLine();
                }
                else
                {
                    Debug.LogWarning("[SubtitleManager] Dialogue vide ou invalide");
                    CleanupWebGLVideo();
                    OnDialogueComplete?.Invoke();
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"[SubtitleManager] Erreur parsing dialogue: {e.Message}");
                CleanupWebGLVideo();
                OnDialogueComplete?.Invoke();
            }
        }
        if (token == _token) _loadDialogueCoroutine = null;
    }

    // Convertir le format dialogue vers le format segment existant
    private Segment ConvertDialogueToSegment(DialogueData dialogueData)
    {
        var segment = new Segment
        {
            id = "dialogue",
            title = dialogueData.title,
            speaker = dialogueData.speaker,
            video = dialogueData.video,
            lines = new List<SubLine>()
        };
        
        foreach (var line in dialogueData.lines)
        {
            segment.lines.Add(new SubLine 
            { 
                text = line.text, 
                image = line.image 
            });
        }
        
        return segment;
    }

    private float dialogueStartTime;
    private float maxDialogueDuration = 300f; // 5 minutes max par sécurité
    private bool dialogueConfigApplied = false; // Flag pour éviter double création du cadre

    // Watchdog: certains scripts nettoient/désactivent l'UI de dialogue en début de scène.
    // On répare automatiquement (sans attendre un clic), surtout pour la 1ère ligne.
    private float lastUiRepairTime = -999f;
    private int lastUiRepairSegIndex = -999;
    private int lastUiRepairLineIndex = -999;

    // Choisit un Canvas "stable" pour le bandeau (ne PAS utiliser SceneTransitionOverlay,
    // sinon le bandeau est détruit quand l'overlay de transition fade-out).
    private Canvas FindDialogueHostCanvas()
    {
        Canvas best = null;

        // Unity 2023+: FindObjectsByType, sinon fallback plus bas
        Canvas[] canvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None);
        foreach (var c in canvases)
        {
            if (c == null) continue;
            // Ignore l'overlay de transition (sortingOrder énorme + nom explicite)
            if (c.gameObject != null)
            {
                string n = c.gameObject.name ?? "";
                if (n.Contains("SceneTransitionOverlay")) continue;
                if (n.Contains("TransitionOverlay")) continue;
            }
            if (c.sortingOrder >= 30000) continue;

            // Préférences : ScreenSpaceOverlay + CanvasScaler
            bool cIsOverlay = (c.renderMode == RenderMode.ScreenSpaceOverlay);
            bool cHasScaler = (c.GetComponent<UnityEngine.UI.CanvasScaler>() != null);

            if (best == null)
            {
                best = c;
                continue;
            }

            bool bestIsOverlay = (best.renderMode == RenderMode.ScreenSpaceOverlay);
            bool bestHasScaler = (best.GetComponent<UnityEngine.UI.CanvasScaler>() != null);

            if (cHasScaler && !bestHasScaler) { best = c; continue; }
            if (cIsOverlay && !bestIsOverlay) { best = c; continue; }

            // Sinon, prendre le plus "bas" sortingOrder (souvent le Canvas UI principal)
            if (c.sortingOrder < best.sortingOrder) { best = c; continue; }
        }

        if (best != null) return best;

        // Fallback (au cas où)
        return FindFirstObjectByType<Canvas>();
    }
    
    private void Update()
    {
        // Toggle debug logs ciblés (utile pour traquer qui masque/détruit le cadre)
        var kbToggle = Keyboard.current;
        if (kbToggle != null)
        {
            if (kbToggle[Key.F10].wasPressedThisFrame)
            {
                DialogueDebugLog.Enabled = !DialogueDebugLog.Enabled;
                Debug.Log($"[DialogueDebug] Enabled={DialogueDebugLog.Enabled} (F10)");
            }
            if (kbToggle[Key.F9].wasPressedThisFrame)
            {
                DialogueDebugLog.IncludeStackTrace = !DialogueDebugLog.IncludeStackTrace;
                Debug.Log($"[DialogueDebug] IncludeStackTrace={DialogueDebugLog.IncludeStackTrace} (F9)");
            }
        }

        // Auto-terminer le dialogue si trop de temps passe sans interaction
        if (project != null && segIndex >= 0)
        {
            var seg = CurrentSeg();
            if (seg != null && seg.lines != null && lineIndex < seg.lines.Count)
            {
                float timeSinceStart = Time.time - dialogueStartTime;
                if (timeSinceStart > maxDialogueDuration)
                {
                    Debug.LogWarning("[SubtitleManager] Dialogue expiré après timeout - fermeture automatique");
                    CleanupWebGLVideo();
                    OnDialogueComplete?.Invoke();
                    return;
                }
            }
        }

        // 🔧 Auto-réparation UI (Desktop/Editor)
        // Si le bandeau ou les refs TMP disparaissent "tout seul", on réaffiche la ligne courante.
        if (!IsWebGL && IsPlayingDialogue())
        {
            GameObject banner = GameObject.Find("DialogueBottomBanner");
            bool bannerOk = (banner != null && banner.activeInHierarchy);
            bool refsOk = (speakerText != null && subtitleText != null);

            if (!bannerOk || !refsOk)
            {
                // Debounce pour éviter de spammer/recréer en boucle
                float now = Time.unscaledTime;
                bool indexChanged = (segIndex != lastUiRepairSegIndex || lineIndex != lastUiRepairLineIndex);
                if (indexChanged || (now - lastUiRepairTime) > 0.15f)
                {
                    lastUiRepairTime = now;
                    lastUiRepairSegIndex = segIndex;
                    lastUiRepairLineIndex = lineIndex;

                    DialogueDebugLog.Warn($"[SubtitleManager] Watchdog UI: bannerOk={bannerOk}, refsOk={refsOk} -> réaffichage ligne courante (seg={segIndex}, line={lineIndex})");
                    ApplyCurrentLine();
                }
            }
        }
        
        var kb    = Keyboard.current;
        var mouse = Mouse.current;
        var touch = Touchscreen.current;

        bool next = false, prev = false;

        if (kb != null)
        {
            if (kb[nextKey].wasPressedThisFrame) next = true;
            if (kb[prevKey].wasPressedThisFrame) prev = true;
            if (kb[Key.RightArrow].wasPressedThisFrame) next = true;
            if (kb[Key.LeftArrow].wasPressedThisFrame)  prev = true;
            if (kb[Key.Enter].wasPressedThisFrame || kb[Key.NumpadEnter].wasPressedThisFrame) next = true;
        }
        if (mouse != null)
        {
            if (mouse.leftButton.wasPressedThisFrame)  next = true;
            if (mouse.rightButton.wasPressedThisFrame) prev = true;
        }
        if (touch != null && touch.primaryTouch.press.wasPressedThisFrame) next = true;

        if (next)
        {
            Debug.Log("[SubtitleManager] Input détecté: Next");
            Next();
        }
        else if (prev)
        {
            Debug.Log("[SubtitleManager] Input détecté: Prev");
            Prev();
        }

        // TEST : Touche F pour forcer la fin du dialogue (nouveau Input System)
        if (kb != null && kb[Key.F].wasPressedThisFrame)
        {
            Debug.Log("[TEST] Fin forcée du dialogue");
            CleanupWebGLVideo();
            OnDialogueComplete?.Invoke();
        }
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // API pour WebGL et autres systèmes externes
    // ─────────────────────────────────────────────────────────────────────────────
    
    /// <summary>
    /// Vérifie si un dialogue est en cours de lecture
    /// </summary>
    public bool IsPlayingDialogue()
    {
        var seg = CurrentSeg();
        bool isPlaying = (seg != null && seg.lines != null && lineIndex >= 0 && lineIndex < seg.lines.Count);
        return isPlaying;
    }
    
    /// <summary>
    /// Force le masquage de tous les éléments UI du SubtitleManager
    /// </summary>
    public void ForceHideAllUI()
    {
        Debug.Log("[SubtitleManager] ForceHideAllUI() - Nettoyage complet de l'UI");
        DialogueDebugLog.Critical("[SubtitleManager] ForceHideAllUI() appelé -> quelqu'un tente de masquer l'UI de dialogue");
        
        // Cacher le DialogueBottomBanner
        GameObject bottomBanner = GameObject.Find("DialogueBottomBanner");
        if (bottomBanner != null)
        {
            Debug.Log("[SubtitleManager] DialogueBottomBanner trouvé et désactivé");
            bottomBanner.SetActive(false);
        }
        
        // Cacher les éléments Unity (Desktop/Editor)
        if (speakerText != null)
        {
            speakerText.gameObject.SetActive(false);
        }
        
        if (subtitleText != null)
        {
            subtitleText.gameObject.SetActive(false);
        }
        
        if (overlayImage != null)
        {
            overlayImage.gameObject.SetActive(false);
            overlayImage.sprite = null;
        }
        
        if (videoSurface != null)
        {
            videoSurface.gameObject.SetActive(false);
            videoSurface.texture = null;
        }
        
        // Arrêter la vidéo
        if (videoPlayer != null)
        {
            videoPlayer.Stop();
        }
        
        // Nettoyer la vidéo WebGL
        CleanupWebGLVideo();
        
        // 🔧 Réinitialiser le flag de configuration pour forcer la recréation du banner au prochain dialogue
        dialogueConfigApplied = false;
        Debug.Log("[SubtitleManager] dialogueConfigApplied réinitialisé à false");
        
        // Réinitialiser l'état
        project = null;
        segIndex = -1;
        lineIndex = -1;
    }
    
    /// <summary>
    /// Appelé par WebGLClickReceiver quand un clic est détecté en WebGL
    /// </summary>
    public void OnWebGLClick()
    {
        if (!IsPlayingDialogue()) return;
        
        Debug.Log("[SubtitleManager] OnWebGLClick reçu - avancement vers la ligne suivante");
        Next();
    }
    
    // ─────────────────────────────────────────────────────────────────────────────
    // Navigation
    // ─────────────────────────────────────────────────────────────────────────────
    private void Next()
    {
        var seg = CurrentSeg(); 
        if (seg == null || seg.lines == null || seg.lines.Count == 0)
        {
            Debug.LogWarning("[SubtitleManager] Next() appelé mais seg invalide");
            return;
        }

        // 🔧 Si l'UI a été détruite/désactivée entre temps (cas du "1er sous-titre qui disparaît"),
        // on reconstruit/force l'affichage SANS avancer d'abord, sinon on "perd" la 1ère ligne.
        GameObject banner = GameObject.Find("DialogueBottomBanner");
        bool bannerOk = (banner != null && banner.activeInHierarchy);
        bool refsOk = (speakerText != null && subtitleText != null);
        if (!bannerOk || !refsOk)
        {
            Debug.LogWarning($"[SubtitleManager] Next() détecte UI invalide (bannerOk={bannerOk}, refsOk={refsOk}) -> réaffichage ligne courante sans avancer");
            ApplyCurrentLine();
            return;
        }
        
        Debug.Log($"[SubtitleManager] Next() - ligne actuelle: {lineIndex}/{seg.lines.Count}");
        
        lineIndex++;
        if (lineIndex >= seg.lines.Count)
        {
            // DIALOGUE TERMINÉ
            Debug.Log("[SubtitleManager] Dialogue terminé - nettoyage vidéo WebGL avant callback");
            CleanupWebGLVideo();
            OnDialogueComplete?.Invoke();
            return;
        }
        
        ApplyCurrentLine();
    }

    private void Prev()
    {
        var seg = CurrentSeg(); if (seg == null || seg.lines == null || seg.lines.Count == 0) return;
        lineIndex--;
        if (lineIndex < 0)
        {
            if (lockedSegmentIndex >= 0) { lineIndex = 0; return; }
            segIndex--;
            if (segIndex < 0)
            {
                if (!loopAllSegments) { segIndex = 0; lineIndex = 0; ApplyCurrentLine(); return; }
                segIndex = project.segments.Count - 1;
            }
            lineIndex = Mathf.Max(0, CurrentSeg().lines.Count - 1);
            LoadSegment(segIndex);
        }
        ApplyCurrentLine();
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Chargement d'un segment (vidéo)
    // ─────────────────────────────────────────────────────────────────────────────
    private void LoadSegment(int index)
    {
        var seg = project.segments[index];
        if (seg == null) return;

        string url = BuildVideoUrl(seg.video);
        if (string.IsNullOrEmpty(url))
        {
            Debug.LogError("[SubtitleManager] URL vidéo vide (segment " + index + ")");
            return;
        }

        if (prepareCoroutine != null) { StopCoroutine(prepareCoroutine); prepareCoroutine = null; }
        videoLoadSeq++;

        if (IsWebGL)
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            if (videoSurface)
            {
                var c = videoSurface.color; c.a = 0f; videoSurface.color = c;
                videoSurface.texture = null;
            }
            try
            {
                WVO_Dispose();
                string fit = letterbox ? "contain" : "cover";
                WVO_Create(url, fit, loopVideoEachSegment ? 1 : 0, 1 /*muted*/);
                WVO_Play();
                Debug.Log("[SubtitleManager] HTML5 video (OVER canvas) -> " + url);
            }
            catch (Exception e)
            {
                Debug.LogError("[SubtitleManager] WVO_Create/Play exception: " + e.Message);
            }
#endif
            lastOverlayImageName = null;
            return;
        }

        videoPlayer.source = VideoSource.Url;
        videoPlayer.isLooping = loopVideoEachSegment;
        videoPlayer.renderMode = VideoRenderMode.RenderTexture;
        videoPlayer.targetTexture = null;

        prepareCoroutine = StartCoroutine(PrepareAndPlayUrl_Desktop(url, videoLoadSeq));
        lastOverlayImageName = null;
    }

    private IEnumerator PrepareAndPlayUrl_Desktop(string url, int expectedSeq)
    {
        videoPlayer.url = url;
        videoPlayer.Prepare();

        float t = 0f, TIMEOUT = 20f;
        while (!videoPlayer.isPrepared && t < TIMEOUT)
        {
            if (expectedSeq != videoLoadSeq) yield break;
            t += Time.unscaledDeltaTime; yield return null;
        }
        if (!videoPlayer.isPrepared)
        {
            Debug.LogError("[SubtitleManager] Impossible de préparer la vidéo (Desktop): " + url);
            yield break;
        }

        int w = Mathf.Max((int)videoPlayer.width, 1), h = Mathf.Max((int)videoPlayer.height, 1);
        if (w <= 1 || h <= 1) { w = 1280; h = 720; }

        RecreateVideoRT(w, h);
        videoPlayer.targetTexture = videoRT;

        if (videoSurface) videoSurface.texture = videoRT;
        ApplyAspectToVideoSurface(w, h);

        videoPlayer.time = 0.0;
        videoPlayer.Play();

        yield return StartCoroutine(FadeRawImage(videoSurface, 0f, 1f, videoFadeInSeconds));
        Debug.Log("[SubtitleManager] Play URL (Desktop): " + url);
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Helpers vidéo (Desktop/Editor)
    // ─────────────────────────────────────────────────────────────────────────────
    private void RecreateVideoRT(int w, int h)
    {
        if (videoRT != null && videoRT.width == w && videoRT.height == h) return;
        CleanupVideoRT();
        videoRT = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32)
        {
            useMipMap = false,
            autoGenerateMips = false,
            wrapMode = TextureWrapMode.Clamp,
            filterMode = FilterMode.Bilinear
        };
        videoRT.Create();
    }

    private void CleanupVideoRT()
    {
        if (videoRT != null)
        {
            if (videoPlayer && videoPlayer.targetTexture == videoRT) videoPlayer.targetTexture = null;
            videoRT.Release();
            Destroy(videoRT);
            videoRT = null;
        }
    }
    
    /// <summary>
    /// Nettoie la vidéo HTML5 en WebGL pour éviter qu'elle reste affichée après la fin du dialogue
    /// </summary>
    private void CleanupWebGLVideo()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        // IMPORTANT: en WebGL, l'image overlay (dom-img / img-overlay) est indépendante du bandeau texte.
        // UI_SetVisible(0) masque le bandeau, mais ne retire pas forcément l'overlay image.
        // => On doit explicitement la cacher pour éviter qu'elle reste visible au retour sur Map.
        try
        {
            UI_HideOverlayImage(0); // immédiat : tout doit être effacé avant retour scène Map
        }
        catch (Exception e)
        {
            Debug.LogWarning($"[SubtitleManager] Erreur UI_HideOverlayImage: {e.Message}");
        }

        try 
        { 
            Debug.Log("[SubtitleManager] Nettoyage vidéo WebGL: UI_SetVisible(0)");
            UI_SetVisible(0); 
        } 
        catch (Exception e)
        {
            Debug.LogWarning($"[SubtitleManager] Erreur UI_SetVisible: {e.Message}");
        }
        
        try 
        { 
            Debug.Log("[SubtitleManager] Nettoyage vidéo WebGL: WVO_Dispose()");
            WVO_Dispose(); 
        } 
        catch (Exception e)
        {
            Debug.LogWarning($"[SubtitleManager] Erreur WVO_Dispose: {e.Message}");
        }
#endif
    }

    private IEnumerator FadeRawImage(RawImage img, float from, float to, float duration)
    {
        if (!img) yield break;
        Color c = img.color; c.a = from; img.color = c;
        if (duration <= 0.0001f) { c.a = to; img.color = c; yield break; }
        float t = 0f;
        while (t < duration)
        {
            t += Time.unscaledDeltaTime;
            c.a = Mathf.Lerp(from, to, Mathf.Clamp01(t / duration));
            img.color = c;
            yield return null;
        }
        c.a = to; img.color = c;
    }

    private void ApplyAspectToVideoSurface(int w, int h)
    {
        if (!videoSurface) return;
        if (!videoAspect) videoAspect = videoSurface.GetComponent<AspectRatioFitter>() ?? videoSurface.gameObject.AddComponent<AspectRatioFitter>();
        videoAspect.aspectMode  = letterbox ? AspectRatioFitter.AspectMode.FitInParent : AspectRatioFitter.AspectMode.EnvelopeParent;
        videoAspect.aspectRatio = Mathf.Max(0.01f, (float)w / Mathf.Max(1, h));
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Affichage (speaker / sous-titres / overlay image)
    // ─────────────────────────────────────────────────────────────────────────────
    private void ApplyCurrentLine()
    {
        var seg = CurrentSeg(); if (seg == null || seg.lines == null || seg.lines.Count == 0) return;
        lineIndex = Mathf.Clamp(lineIndex, 0, seg.lines.Count - 1);
        var line = seg.lines[lineIndex];

        Debug.Log($"[SubtitleManager] ApplyCurrentLine - speaker='{seg.speaker}', text='{line.text}'");

#if UNITY_WEBGL && !UNITY_EDITOR
        string speakerHtml  = string.IsNullOrEmpty(seg.speaker) ? "" : EscapeBasicHtml(seg.speaker, allowTags:false);
        string subtitleHtml = ConvertTmpToHtml(line.text ?? "");
        try {
            UI_SetSpeakerHtml(speakerHtml);
            UI_SetSubtitleHtml(subtitleHtml);
            Debug.Log($"[DOM] speaker='{seg.speaker}' subtitleHtml='{subtitleHtml}'");
        } catch {}
#else
        // 🔧 S'assurer que l'UI existe AVANT d'écrire dans les textes (sinon NULL + invisible)
        EnsureDialogueConfigApplied();

        // 🔧 S'assurer que le bandeau est actif (certains nettoyages le désactivent)
        GameObject bottomBanner = GameObject.Find("DialogueBottomBanner");
        if (bottomBanner != null && !bottomBanner.activeSelf)
        {
            bottomBanner.SetActive(true);
        }

        Debug.Log($"[SubtitleManager] speakerText null? {speakerText == null}, subtitleText null? {subtitleText == null}");
        if (speakerText)
        {
            speakerText.text = string.IsNullOrEmpty(seg.speaker) ? "" : seg.speaker;
            Debug.Log($"[SubtitleManager] ✅ Speaker text mis à jour: '{speakerText.text}' - GO actif: {speakerText.gameObject.activeInHierarchy}");
            if (!speakerText.gameObject.activeSelf) speakerText.gameObject.SetActive(true);
        }
        else
        {
            Debug.LogError("[SubtitleManager] ❌ speakerText est NULL !");
        }
        if (subtitleText)
        {
            subtitleText.text = line.text ?? "";
            Debug.Log($"[SubtitleManager] ✅ Subtitle text mis à jour: '{subtitleText.text}' - GO actif: {subtitleText.gameObject.activeInHierarchy}");
            if (!subtitleText.gameObject.activeSelf) subtitleText.gameObject.SetActive(true);
        }
        else
        {
            Debug.LogError("[SubtitleManager] ❌ subtitleText est NULL !");
        }
#endif

        HandleOverlayImage(line.image);
    }

    private void HandleOverlayImage(string imageNameOrUrl)
    {
        string fullUrlForWeb = BuildImageUrl(imageNameOrUrl);
        string keyForSkip    = string.IsNullOrEmpty(imageNameOrUrl) ? "" : imageNameOrUrl.Trim();

        if (IsWebGL)
        {
#if UNITY_WEBGL && !UNITY_EDITOR
            int fadeMs = Mathf.RoundToInt(Mathf.Clamp01(overlayFadeInDuration) * 1000f);
            if (string.IsNullOrEmpty(fullUrlForWeb))
            {
                try { UI_HideOverlayImage(fadeMs); } catch {}
                lastOverlayImageName = null;
                return;
            }
            int skip = (lastOverlayImageName != null && lastOverlayImageName == keyForSkip) ? 1 : 0;
            try {
                Debug.Log("[DOM IMG req] " + fullUrlForWeb);
                UI_ShowOverlayImage(fullUrlForWeb, fadeMs, skip);
                
                // Positionner l'image selon la config du JSON
                float posX = 1585f; // Valeur par défaut
                float posY = 400f;  // Valeur par défaut
                float diameter = 160f;
                float borderW = 4f;
                string borderCol = "#F4ECE5";
                string bgCol = "#FF0000";
                string gradStart = "";
                string gradEnd = "";
                if (GeneralConfigManager.Instance != null && GeneralConfigManager.Instance.IsConfigLoaded())
                {
                    var defConfig = GeneralConfigManager.Instance.GetDefaultDialogueConfig();
                    if (defConfig?.illustrationPosition != null)
                    {
                        posX = defConfig.illustrationPosition.x;
                        posY = defConfig.illustrationPosition.y;
                    }
                    // Diamètre unique prioritaire, sinon rétro-compat (illustrationSize)
                    if (defConfig != null && defConfig.illustrationDiameter > 0)
                        diameter = defConfig.illustrationDiameter;
                    else if (defConfig?.illustrationSize != null)
                        diameter = defConfig.illustrationSize.x > 0 ? defConfig.illustrationSize.x : defConfig.illustrationSize.y;
                    if (defConfig?.illustrationBorder != null)
                    {
                        borderW = defConfig.illustrationBorder.width;
                        borderCol = defConfig.illustrationBorder.color;
                    }
                    if (!string.IsNullOrEmpty(defConfig?.illustrationBackgroundColor))
                    {
                        bgCol = defConfig.illustrationBackgroundColor;
                    }
                    if (defConfig?.illustrationBackgroundGradient != null)
                    {
                        gradStart = defConfig.illustrationBackgroundGradient.startColor ?? "";
                        gradEnd = defConfig.illustrationBackgroundGradient.endColor ?? "";
                    }
                }
                UI_SetOverlayImagePosition(posX, posY);
                // Appliquer le style (taille + bordure + fond) à l'image DOM.
                try { UI_SetOverlayImageStyle(diameter, diameter, borderW, borderCol, bgCol, gradStart, gradEnd); } catch {}
                Debug.Log($"[DOM IMG] Position: x={posX}, y={posY}");
            } catch (Exception e) {
                Debug.LogError("[DOM IMG] exception " + e.Message);
            }
            lastOverlayImageName = keyForSkip;
#endif
            return;
        }

        if (!overlayImage) return;

        if (string.IsNullOrEmpty(imageNameOrUrl))
        {
            StopOverlayFadeIfAny(); SetOverlaySprite(null, 0f); lastOverlayImageName = null; return;
        }

        _token++;
        if (_overlayLoadCoroutine != null) StopCoroutine(_overlayLoadCoroutine);
        _overlayLoadCoroutine = StartCoroutine(LoadOverlaySprite_Desktop(imageNameOrUrl));
    }

    private IEnumerator LoadOverlaySprite_Desktop(string imageNameOrUrl)
    {
        int token = _token;
        var candidates = BuildImageUrlCandidates(imageNameOrUrl);
        if (candidates.Count == 0)
        {
            Debug.LogWarning("[SubtitleManager] Aucun candidat URL pour l'image: " + imageNameOrUrl);
            yield break;
        }

        bool wasVisible = !string.IsNullOrEmpty(lastOverlayImageName);
        lastOverlayImageName = imageNameOrUrl.Trim();

        foreach (var url in candidates)
        {
            if (token != _token) yield break;
            if (spriteCache.TryGetValue(url, out var cached))
            {
                overlayImage.sprite = cached;
                PositionOverlayImage(); // Positionner selon la config
                if (!wasVisible)
                {
                    if (overlayFadeCoroutine != null) StopCoroutine(overlayFadeCoroutine);
                    overlayFadeCoroutine = StartCoroutine(FadeOverlayAlpha(0f, 1f, overlayFadeInDuration));
                    TrySpawnSparkleFXAtOverlay();
                }
                else SetOverlayAlpha(1f);
                yield break;
            }
        }

        Texture2D tex = null; string usedUrl = null;
        foreach (var url in candidates)
        {
            if (token != _token) yield break;
            using (var req = UnityWebRequestTexture.GetTexture(url))
            {
                yield return req.SendWebRequest();
                if (token != _token) yield break;
#if UNITY_2020_2_OR_NEWER
                if (req.result != UnityWebRequest.Result.Success)
#else
                if (req.isNetworkError || req.isHttpError)
#endif
                {
                    Debug.LogWarning("[SubtitleManager] Échec image: " + url + " -> " + req.error);
                    continue;
                }
                tex = DownloadHandlerTexture.GetContent(req);
                usedUrl = url;
                break;
            }
        }

        if (tex == null)
        {
            Debug.LogWarning("[SubtitleManager] Impossible de charger l'image overlay.");
            StopOverlayFadeIfAny(); SetOverlaySprite(null, 0f);
            yield break;
        }
        if (token != _token) yield break;

        var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
        sprite.name = "Overlay:" + Path.GetFileName(usedUrl);
        spriteCache[usedUrl] = sprite;

        overlayImage.sprite = sprite;
        
        // Positionner l'image selon la configuration de general-config.json
        PositionOverlayImage();
        
        if (!wasVisible)
        {
            if (overlayFadeCoroutine != null) StopCoroutine(overlayFadeCoroutine);
            overlayFadeCoroutine = StartCoroutine(FadeOverlayAlpha(0f, 1f, overlayFadeInDuration));
            TrySpawnSparkleFXAtOverlay();
        }
        else SetOverlayAlpha(1f);

        if (token == _token) _overlayLoadCoroutine = null;
    }
    
    /// <summary>
    /// Positionne l'image overlay selon la configuration de general-config.json
    /// </summary>
    private void PositionOverlayImage()
    {
        if (overlayImage == null) return;
        
        // S'assurer que le conteneur a un Canvas override pour s'afficher AU-DESSUS du cadre des sous-titres (sortingOrder 1000)
        Canvas overlayCanvas = (overlayImageContainer != null) ? overlayImageContainer.GetComponent<Canvas>() : overlayImage.GetComponent<Canvas>();
        if (overlayCanvas == null)
        {
            var target = (overlayImageContainer != null) ? overlayImageContainer : overlayImage.gameObject;
            overlayCanvas = target.AddComponent<Canvas>();
            overlayCanvas.overrideSorting = true;
            overlayCanvas.sortingOrder = 1100; // Au-dessus du cadre de dialogue (1000)
            target.AddComponent<UnityEngine.UI.GraphicRaycaster>();
            Debug.Log("[SubtitleManager] ✅ Canvas override ajouté à overlayImage (sortingOrder=1100)");
        }
        
        // Valeurs par défaut
        float posX = 1465f;
        float posY = 670f;
        float diameter = 160f;
        float borderW = 4f;
        string borderCol = "#F4ECE5";
        string bgCol = "#FF0000";
        // Gradient (optionnel)
        string gradStart = "";
        string gradEnd = "";
        
        // Essayer de charger depuis GeneralConfigManager
        if (GeneralConfigManager.Instance != null && GeneralConfigManager.Instance.IsConfigLoaded())
        {
            var defConfig = GeneralConfigManager.Instance.GetDefaultDialogueConfig();
            if (defConfig?.illustrationPosition != null)
            {
                posX = defConfig.illustrationPosition.x;
                posY = defConfig.illustrationPosition.y;
            }
            // Diamètre unique prioritaire, sinon rétro-compat (illustrationSize)
            if (defConfig != null && defConfig.illustrationDiameter > 0)
                diameter = defConfig.illustrationDiameter;
            else if (defConfig?.illustrationSize != null)
                diameter = defConfig.illustrationSize.x > 0 ? defConfig.illustrationSize.x : defConfig.illustrationSize.y;
            if (defConfig?.illustrationBorder != null)
            {
                borderW = defConfig.illustrationBorder.width;
                borderCol = defConfig.illustrationBorder.color;
            }
            if (!string.IsNullOrEmpty(defConfig?.illustrationBackgroundColor))
            {
                bgCol = defConfig.illustrationBackgroundColor;
            }
            if (defConfig?.illustrationBackgroundGradient != null)
            {
                gradStart = defConfig.illustrationBackgroundGradient.startColor ?? "";
                gradEnd = defConfig.illustrationBackgroundGradient.endColor ?? "";
            }
        }
        
        // Conversion : position par rapport au centre du canvas (960, 540 en 1920x1080)
        float anchoredX = posX - 960f;
        float anchoredY = posY - 540f;
        
        RectTransform imgRect = (overlayImageContainer != null)
            ? overlayImageContainer.GetComponent<RectTransform>()
            : overlayImage.rectTransform;
        imgRect.anchorMin = new Vector2(0.5f, 0.5f);
        imgRect.anchorMax = new Vector2(0.5f, 0.5f);
        imgRect.pivot = new Vector2(0.5f, 0.5f);
        imgRect.anchoredPosition = new Vector2(anchoredX, anchoredY);
        imgRect.sizeDelta = new Vector2(diameter, diameter);

        // Appliquer style bordure + fond (Desktop/Editor)
        overlayBorderWidthPx = Mathf.Max(0f, borderW);
        if (overlayInnerMaskRect != null)
        {
            overlayInnerMaskRect.offsetMin = new Vector2(overlayBorderWidthPx, overlayBorderWidthPx);
            overlayInnerMaskRect.offsetMax = new Vector2(-overlayBorderWidthPx, -overlayBorderWidthPx);
        }
        if (overlayBorderGraphic != null)
        {
            if (ColorUtility.TryParseHtmlString(borderCol, out Color bc) || ColorUtility.TryParseHtmlString("#" + borderCol.TrimStart('#'), out bc))
                overlayBorderGraphic.color = bc;
        }
        // Fond: gradient si configuré, sinon couleur unie
        if (overlayMaskGraphic != null) overlayMaskGraphic.color = Color.white; // mask shape uniquement
        Image bg = overlayInnerMaskRect != null ? overlayInnerMaskRect.GetComponentInChildren<Image>() : null;
        if (bg != null)
        {
            bool hasGrad = !string.IsNullOrEmpty(gradStart) && !string.IsNullOrEmpty(gradEnd);
            if (hasGrad)
            {
                if (ColorUtility.TryParseHtmlString(gradStart, out Color top) || ColorUtility.TryParseHtmlString("#" + gradStart.TrimStart('#'), out top))
                {
                    if (ColorUtility.TryParseHtmlString(gradEnd, out Color bottom) || ColorUtility.TryParseHtmlString("#" + gradEnd.TrimStart('#'), out bottom))
                    {
                        EnsureOverlayGradientSprite(Mathf.RoundToInt(diameter), top, bottom);
                        bg.sprite = overlayGradientSprite;
                        bg.color = Color.white;
                        goto __done;
                    }
                }
            }
            bg.sprite = null;
            if (ColorUtility.TryParseHtmlString(bgCol, out Color uni) || ColorUtility.TryParseHtmlString("#" + bgCol.TrimStart('#'), out uni))
                bg.color = uni;
        }
__done: ;
        
        Debug.Log($"[SubtitleManager] 🖼️ OverlayImage positionnée à ({posX}, {posY}) -> anchoredPosition ({anchoredX}, {anchoredY})");
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Conversions texte (TMP → HTML pour Web)
    // ─────────────────────────────────────────────────────────────────────────────
    private string ConvertTmpToHtml(string s)
    {
        if (string.IsNullOrEmpty(s)) return "";
        string h = s;

        h = Regex.Replace(h, @"\[c=([^\]]+)\]", "<color=$1>", RegexOptions.IgnoreCase);
        h = h.Replace("[/c]", "</color>");

        h = h.Replace("\r\n", "\n").Replace("\n", "<br>");

        h = Regex.Replace(h, @"<color=([^>]+)>", "<span style=\"color:$1\">", RegexOptions.IgnoreCase);
        h = Regex.Replace(h, @"</color>", "</span>", RegexOptions.IgnoreCase);

        h = Regex.Replace(h, @"<\#([0-9a-fA-F]{3,8})>",  "<span style=\"color:#$1\">");

        h = Regex.Replace(h, @"<size=([^>]+)>", "<span style=\"font-size:$1\">", RegexOptions.IgnoreCase);
        h = Regex.Replace(h, @"</size>", "</span>", RegexOptions.IgnoreCase);

        return h;
    }
    private string EscapeBasicHtml(string s, bool allowTags) => allowTags ? s : System.Net.WebUtility.HtmlEncode(s);

    // ─────────────────────────────────────────────────────────────────────────────
    // Overlay Unity (helpers Desktop)
    // ─────────────────────────────────────────────────────────────────────────────
    private void StopOverlayFadeIfAny() { if (overlayFadeCoroutine != null) StopCoroutine(overlayFadeCoroutine); overlayFadeCoroutine = null; }
    private void SetOverlaySprite(Sprite s, float a) { overlayImage.sprite = s; SetOverlayAlpha(a); }
    private void SetOverlayAlpha(float a)
    {
        float alpha = Mathf.Clamp01(a);
        if (overlayImageCanvasGroup != null)
        {
            overlayImageCanvasGroup.alpha = alpha;
        }
        else if (overlayImage != null)
        {
            var c = overlayImage.color; c.a = alpha; overlayImage.color = c;
        }
    }

    private void SetupOverlayImageCircleContainer()
    {
        if (overlayImage == null) return;
        if (overlayImageContainer != null) return;

        Transform parent = overlayImage.transform.parent;
        if (parent == null) return;

        // Container au même parent, puis on y met bordure + masque + l'image.
        overlayImageContainer = new GameObject("OverlayImageCircle");
        overlayImageContainer.transform.SetParent(parent, false);
        overlayImageContainer.transform.SetAsLastSibling();

        RectTransform containerRect = overlayImageContainer.AddComponent<RectTransform>();
        containerRect.anchorMin = overlayImage.rectTransform.anchorMin;
        containerRect.anchorMax = overlayImage.rectTransform.anchorMax;
        containerRect.pivot = overlayImage.rectTransform.pivot;
        containerRect.anchoredPosition = overlayImage.rectTransform.anchoredPosition;
        containerRect.sizeDelta = overlayImage.rectTransform.sizeDelta == Vector2.zero ? new Vector2(160, 160) : overlayImage.rectTransform.sizeDelta;

        overlayImageCanvasGroup = overlayImageContainer.AddComponent<CanvasGroup>();
        overlayImageCanvasGroup.alpha = 0f;

        // Bordure
        var borderObj = new GameObject("Border");
        borderObj.transform.SetParent(overlayImageContainer.transform, false);
        var borderRect = borderObj.AddComponent<RectTransform>();
        borderRect.anchorMin = Vector2.zero;
        borderRect.anchorMax = Vector2.one;
        borderRect.offsetMin = Vector2.zero;
        borderRect.offsetMax = Vector2.zero;
        overlayBorderGraphic = borderObj.AddComponent<CircleMaskGraphic>();
        overlayBorderGraphic.color = new Color(0.956f, 0.925f, 0.898f, 1f); // #F4ECE5 fallback
        overlayBorderGraphic.raycastTarget = false;

        // Masque intérieur (fond + clip)
        var maskObj = new GameObject("MaskCircle");
        maskObj.transform.SetParent(overlayImageContainer.transform, false);
        overlayInnerMaskRect = maskObj.AddComponent<RectTransform>();
        overlayInnerMaskRect.anchorMin = Vector2.zero;
        overlayInnerMaskRect.anchorMax = Vector2.one;
        overlayInnerMaskRect.offsetMin = new Vector2(overlayBorderWidthPx, overlayBorderWidthPx);
        overlayInnerMaskRect.offsetMax = new Vector2(-overlayBorderWidthPx, -overlayBorderWidthPx);

        overlayMaskGraphic = maskObj.AddComponent<CircleMaskGraphic>();
        overlayMaskGraphic.color = Color.white; // mask shape uniquement
        overlayMaskGraphic.raycastTarget = false;

        var mask = maskObj.AddComponent<Mask>();
        mask.showMaskGraphic = false;

        // Fond (dans le mask)
        var bgObj = new GameObject("Background");
        bgObj.transform.SetParent(maskObj.transform, false);
        var bgRect = bgObj.AddComponent<RectTransform>();
        bgRect.anchorMin = Vector2.zero;
        bgRect.anchorMax = Vector2.one;
        bgRect.offsetMin = Vector2.zero;
        bgRect.offsetMax = Vector2.zero;
        var bgImg = bgObj.AddComponent<Image>();
        bgImg.raycastTarget = false;
        bgImg.color = Color.red; // fallback (override dans PositionOverlayImage)

        // Reparent l'image overlay sous le masque
        overlayImage.transform.SetParent(maskObj.transform, false);
        var imgRect = overlayImage.rectTransform;
        imgRect.anchorMin = Vector2.zero;
        imgRect.anchorMax = Vector2.one;
        imgRect.offsetMin = Vector2.zero;
        imgRect.offsetMax = Vector2.zero;
        overlayImage.enabled = true;
    }

    private Texture2D overlayGradientTex;
    private Sprite overlayGradientSprite;
    private int overlayGradientDiameter = -1;

    private void EnsureOverlayGradientSprite(int diameter, Color top, Color bottom)
    {
        int d = Mathf.Max(8, diameter);
        if (overlayGradientSprite != null && overlayGradientDiameter == d) return;
        if (overlayGradientSprite != null) { Destroy(overlayGradientSprite); overlayGradientSprite = null; }
        if (overlayGradientTex != null) { Destroy(overlayGradientTex); overlayGradientTex = null; }

        overlayGradientDiameter = d;
        overlayGradientTex = new Texture2D(d, d, TextureFormat.RGBA32, false);
        overlayGradientTex.filterMode = FilterMode.Bilinear;
        overlayGradientTex.wrapMode = TextureWrapMode.Clamp;

        Color[] pixels = new Color[d * d];
        for (int y = 0; y < d; y++)
        {
            float t = d <= 1 ? 0f : (float)y / (d - 1);
            Color c = Color.Lerp(bottom, top, t);
            for (int x = 0; x < d; x++)
                pixels[y * d + x] = c;
        }
        overlayGradientTex.SetPixels(pixels);
        overlayGradientTex.Apply();
        overlayGradientSprite = Sprite.Create(overlayGradientTex, new Rect(0, 0, d, d), new Vector2(0.5f, 0.5f), 100f);
    }

    private IEnumerator FadeOverlayAlpha(float from, float to, float duration)
    {
        if (!overlayImage) yield break;
        float t = 0f;
        SetOverlayAlpha(from);
        if (duration <= 0.0001f) { SetOverlayAlpha(to); yield break; }
        while (t < duration)
        {
            t += Time.deltaTime;
            SetOverlayAlpha(Mathf.Lerp(from, to, Mathf.Clamp01(t / duration)));
            yield return null;
        }
        SetOverlayAlpha(to);
    }

    private void TrySpawnSparkleFXAtOverlay()
    {
        if (!sparklePrefab || !overlayImage) return;
        var cam = fxCamera ? fxCamera : Camera.main; if (!cam) return;

        RectTransform rt = overlayImage.rectTransform;
        Vector2 screenCenter = RectTransformUtility.WorldToScreenPoint(null, rt.TransformPoint(rt.rect.center));
        Vector2 sizePx = rt.rect.size;

        Vector3 worldCenter = cam.ScreenToWorldPoint(new Vector3(screenCenter.x, screenCenter.y, fxSpawnDistance));
        Vector3 worldRight  = cam.ScreenToWorldPoint(new Vector3(screenCenter.x + sizePx.x * 0.5f, screenCenter.y, fxSpawnDistance));
        Vector3 worldTop    = cam.ScreenToWorldPoint(new Vector3(screenCenter.x, screenCenter.y + sizePx.y * 0.5f, fxSpawnDistance));

        float widthWorld  = Mathf.Abs(worldRight.x - worldCenter.x) * 2f * fxGlowExpand;
        float heightWorld = Mathf.Abs(worldTop.y   - worldCenter.y) * 2f * fxGlowExpand;

        var ps = Instantiate(sparklePrefab, worldCenter, Quaternion.identity);
        ps.transform.forward = cam.transform.forward;

        var shape = ps.shape;
        shape.shapeType = ParticleSystemShapeType.Box;
        shape.scale = new Vector3(Mathf.Max(0.001f, widthWorld), Mathf.Max(0.001f, heightWorld), 0.01f);

        ps.Play();
        Destroy(ps.gameObject, ps.main.duration + ps.main.startLifetime.constantMax + 0.5f);
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // JSON & construction d'URL
    // ─────────────────────────────────────────────────────────────────────────────
    private IEnumerator LoadJson()
    {
        string path = Path.Combine(Application.streamingAssetsPath, jsonFileName);
        string json = null;

        if (path.Contains("://") || path.Contains(":///"))
        {
            using (var www = UnityWebRequest.Get(path))
            {
                yield return www.SendWebRequest();
#if UNITY_2020_2_OR_NEWER
                if (www.result != UnityWebRequest.Result.Success)
#else
                if (www.isNetworkError || www.isHttpError)
#endif
                { Debug.LogError("[SubtitleManager] Erreur JSON: " + www.error); yield break; }
                json = www.downloadHandler.text;
            }
        }
        else
        {
            try { json = File.ReadAllText(path, System.Text.Encoding.UTF8); }
            catch (Exception e) { Debug.LogError("[SubtitleManager] Lecture JSON: " + e.Message); yield break; }
        }

        try { project = JsonUtility.FromJson<Project>(json); }
        catch (Exception e) { Debug.LogError("[SubtitleManager] Parsing JSON: " + e.Message); }
    }

    private string BuildVideoUrl(string fileOrUrl)
    {
        string f = (fileOrUrl ?? "").Trim();
        if (string.IsNullOrEmpty(f)) return "";

        if (IsAbsoluteUrl(f)) return f;
        if (!HasExtension(f)) f += ".mp4";

        if (!string.IsNullOrEmpty(project?.videoRoot))
            return CombineUrl(project.videoRoot, f);

        string local = Path.Combine(Application.streamingAssetsPath, f).Replace("\\", "/");
        if (!local.Contains("://")) local = "file://" + local;
        return local;
    }

    private string BuildImageUrl(string fileOrUrl)
    {
        string n = (fileOrUrl ?? "").Trim();
        if (string.IsNullOrEmpty(n)) return "";

        if (IsAbsoluteUrl(n)) return n;
        if (!string.IsNullOrEmpty(project?.imageRoot))
            return CombineUrl(project.imageRoot, n);

        return "StreamingAssets/Images/" + n;
    }

    private List<string> BuildImageUrlCandidates(string fileOrUrl)
    {
        var list = new List<string>();
        string n = (fileOrUrl ?? "").Trim();
        if (string.IsNullOrEmpty(n)) return list;

        if (IsAbsoluteUrl(n))
        {
            if (HasExtension(n)) { list.Add(n); return list; }
            foreach (var e in new[] { ".png", ".jpg", ".jpeg", ".webp", ".gif" }) list.Add(n + e);
            return list;
        }

        if (!string.IsNullOrEmpty(project?.imageRoot))
        {
            if (HasExtension(n)) list.Add(CombineUrl(project.imageRoot, n));
            else foreach (var e in new[] { ".png", ".jpg", ".jpeg", ".webp", ".gif" }) list.Add(CombineUrl(project.imageRoot, n + e));
        }

        string baseSA = Application.streamingAssetsPath.Replace("\\", "/");
        if (HasExtension(n))
        {
            list.Add(ToFileUrl($"{baseSA}/Images/{n}"));
            list.Add(ToFileUrl($"{baseSA}/{n}"));
        }
        else
        {
            foreach (var e in new[] { ".png", ".jpg", ".jpeg", ".webp", ".gif" })
            {
                list.Add(ToFileUrl($"{baseSA}/Images/{n}{e}"));
                list.Add(ToFileUrl($"{baseSA}/{n}{e}"));
            }
        }
        return list;
    }

    private static bool IsAbsoluteUrl(string s) => s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || s.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
    private static bool HasExtension(string s)   => Path.GetExtension(s).Length > 0;

    private static string EnsureSlashEnd(string root) => string.IsNullOrEmpty(root) ? "" : (root.EndsWith("/") ? root : root + "/");
    private static string CombineUrl(string root, string path)
    {
        string r = EnsureSlashEnd(root ?? "");
        string p = (path ?? "").TrimStart('/');
        return r + p;
    }
    private static string ToFileUrl(string p)
    {
        string u = p.Replace("\\", "/");
        if (!u.Contains("://")) u = "file://" + u;
        return u;
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Accès segment
    // ─────────────────────────────────────────────────────────────────────────────
    private Segment CurrentSeg()
    {
        if (project == null || project.segments == null || project.segments.Count == 0) return null;
        segIndex = Mathf.Clamp(segIndex, 0, project.segments.Count - 1);
        return project.segments[segIndex];
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Configuration dialogue depuis levels-config.json
    // ─────────────────────────────────────────────────────────────────────────────
    /// <summary>
    /// S'assure que la config dialogue est appliquée (appelé avant d'afficher les lignes)
    /// </summary>
    private void EnsureDialogueConfigApplied()
    {
        // 🔧 IMPORTANT : le flag peut rester à true alors que l'UI a été détruite/désactivée
        // (retour de scène, nettoyage "stray UI", etc.). Dans ce cas, speakerText/subtitleText
        // deviennent null et les sous-titres "avancent" sans rien afficher.
        GameObject banner = GameObject.Find("DialogueBottomBanner");
        bool bannerOk = (banner != null && banner.activeInHierarchy);
        bool refsOk = (speakerText != null && subtitleText != null);

        if (dialogueConfigApplied && bannerOk && refsOk)
        {
            Debug.Log("[SubtitleManager] Config déjà appliquée, skip");
            return;
        }

        if (dialogueConfigApplied && (!bannerOk || !refsOk))
        {
            Debug.LogWarning($"[SubtitleManager] Config marquée appliquée mais UI invalide (bannerOk={bannerOk}, refsOk={refsOk}) -> réapplication");
            dialogueConfigApplied = false;
        }

        ApplyDialogueConfig(null);
    }
    
    public void ApplyDialogueConfig(DialogueConfig config)
    {
        // Si config null, essayer de charger depuis GeneralConfigManager
        if (config == null)
        {
            Debug.Log("[SubtitleManager] Config null, tentative de chargement depuis GeneralConfigManager...");
            if (GeneralConfigManager.Instance != null && GeneralConfigManager.Instance.IsConfigLoaded())
            {
                var defConfig = GeneralConfigManager.Instance.GetDefaultDialogueConfig();
                if (defConfig != null)
                {
                    config = ConvertDefaultDialogueToConfig(defConfig);
                    Debug.Log($"[SubtitleManager] ✅ Config chargée depuis GeneralConfigManager: useFrameMode={config.useFrameMode}");
                }
            }
        }
        
        if (config == null) 
        {
            // 🔧 Fallback: on crée quand même une config minimale pour éviter que la 1ère ligne
            // soit affichée sans cadre (puis "perdue" au premier clic).
            Debug.LogWarning("[SubtitleManager] ⚠️ Aucune config disponible -> fallback DialogueConfig()");
            config = new DialogueConfig();
        }
        
        // Marquer la config comme appliquée
        dialogueConfigApplied = true;
        
        Debug.Log($"[SubtitleManager] Application de la configuration dialogue - useFrameMode={config.useFrameMode}, frameWidth={config.frameWidth}");
        
        if (speakerText != null)
        {
            speakerText.fontSize = config.speakerTextSize;
            if (ColorUtility.TryParseHtmlString(config.speakerTextColor, out Color speakerColor))
            {
                speakerText.color = speakerColor;
            }
        }
        
        if (subtitleText != null)
        {
            subtitleText.fontSize = config.dialogueTextSize;
            if (ColorUtility.TryParseHtmlString(config.dialogueTextColor, out Color dialogueColor))
            {
                subtitleText.color = dialogueColor;
            }
        }
        
        nextKey = ParseKey(config.nextKey);
        prevKey = ParseKey(config.prevKey);
        
        CreateBottomBanner(config);
        
        // 🎨 Appliquer les styles CSS pour WebGL
#if UNITY_WEBGL && !UNITY_EDITOR
        try
        {
            string dialogueSizeStr = config.dialogueTextSize.ToString("F0") + "px";
            string speakerSizeStr = config.speakerTextSize.ToString("F0") + "px";
            
            UI_SetSubtitleStyle(
                config.bottomBarColor,
                config.backgroundDimming,
                config.speakerTextColor,
                config.dialogueTextColor,
                dialogueSizeStr,
                speakerSizeStr
            );
            Debug.Log($"[SubtitleManager] ✅ Styles CSS appliqués pour WebGL - Dialogue: {dialogueSizeStr}, Speaker: {speakerSizeStr}");
        }
        catch (System.Exception e)
        {
            Debug.LogWarning($"[SubtitleManager] Erreur lors de l'application des styles CSS: {e.Message}");
        }
#endif
        
        Debug.Log($"[SubtitleManager] Config appliquée - nextKey: {config.nextKey}, prevKey: {config.prevKey}");
        Debug.Log($"[SubtitleManager] Couleurs - Speaker: {config.speakerTextColor}, Dialogue: {config.dialogueTextColor}");
        Debug.Log($"[SubtitleManager] Bandeau - Couleur: {config.bottomBarColor}, Transparence: {config.backgroundDimming}");
    }

    private void CreateBottomBanner(DialogueConfig config)
    {
        Canvas canvas = FindDialogueHostCanvas();
        if (canvas == null)
        {
            Debug.LogWarning("[SubtitleManager] Aucun Canvas trouvé pour le bandeau");
            return;
        }
        
        GameObject oldBanner = GameObject.Find("DialogueBottomBanner");
        if (oldBanner != null)
        {
            Debug.Log("[SubtitleManager] Ancien DialogueBottomBanner trouvé, destruction...");
            // 🔧 Ne nuller les refs que si elles appartiennent réellement à l'ancien banner.
            // En mode legacy, speakerText/subtitleText peuvent être assignés dans l'Inspector
            // et ne doivent PAS être perdus.
            bool speakerBelongsToOld = (speakerText != null && speakerText.transform != null && speakerText.transform.IsChildOf(oldBanner.transform));
            bool subtitleBelongsToOld = (subtitleText != null && subtitleText.transform != null && subtitleText.transform.IsChildOf(oldBanner.transform));
            Destroy(oldBanner);
            // 🔧 Réinitialiser les références uniquement si elles vont être détruites avec l'ancien banner
            if (speakerBelongsToOld) speakerText = null;
            if (subtitleBelongsToOld) subtitleText = null;
        }
        
        GameObject bannerObj = new GameObject("DialogueBottomBanner");
        bannerObj.transform.SetParent(canvas.transform, false);

        // 🔎 SONDE DEBUG (filtrable) : permet de voir QUI désactive/détruit le bandeau + stacktrace
        // Activer avec F10 (DialogueDebugLog.Enabled). F9 toggle stacktrace.
        // IMPORTANT: on la met au plus tôt pour capter les OnDisable/OnDestroy.
        var probe = bannerObj.GetComponent<DialogueUIProbe>() ?? bannerObj.AddComponent<DialogueUIProbe>();
        probe.probeName = "DialogueBottomBanner";
        probe.watchAlpha = true;
        
        // Ajouter un Canvas override pour s'assurer que le cadre est visible
        Canvas bannerCanvas = bannerObj.AddComponent<Canvas>();
        bannerCanvas.overrideSorting = true;
        bannerCanvas.sortingOrder = 1000; // Au-dessus de la plupart des éléments
        bannerObj.AddComponent<UnityEngine.UI.GraphicRaycaster>();
        
        // 🔧 IMPORTANT: Activer explicitement le banner dès sa création
        bannerObj.SetActive(true);
        Debug.Log("[SubtitleManager] ✅ DialogueBottomBanner créé et activé");
        
        // Le Canvas a créé automatiquement un RectTransform, on le récupère
        RectTransform bannerRect = bannerObj.GetComponent<RectTransform>();
        if (bannerRect == null)
        {
            bannerRect = bannerObj.AddComponent<RectTransform>();
        }
        
        // Mode frame centré (nouveau layout) ou mode legacy (bandeau pleine largeur)
        if (config.useFrameMode)
        {
            // Mode frame : cadre centré avec dimensions fixes
            bannerRect.anchorMin = new Vector2(0.5f, 0f);
            bannerRect.anchorMax = new Vector2(0.5f, 0f);
            bannerRect.pivot = new Vector2(0.5f, 0f);
            bannerRect.sizeDelta = new Vector2(config.frameWidth, config.frameHeight);
            bannerRect.anchoredPosition = new Vector2(0, config.frameBottomMargin);
            Debug.Log($"[SubtitleManager] ✅ Mode FRAME: {config.frameWidth}x{config.frameHeight} à {config.frameBottomMargin}px du bas");
        }
        else
        {
            // Mode legacy : bandeau pleine largeur
            bannerRect.anchorMin = new Vector2(0f, 0f);
            bannerRect.anchorMax = new Vector2(1f, config.bottomBarHeightRatio);
            bannerRect.offsetMin = Vector2.zero;
            bannerRect.offsetMax = Vector2.zero;
            Debug.Log("[SubtitleManager] Mode BANDEAU legacy activé");
        }
        
        Image bannerImage = bannerObj.AddComponent<Image>();
        
        // Couleur du fond selon le mode
        if (config.useFrameMode)
        {
            if (ColorUtility.TryParseHtmlString(config.frameBackgroundColor, out Color frameColor))
            {
                bannerImage.color = frameColor;
                Debug.Log($"[SubtitleManager] ✅ Fond cadre: {config.frameBackgroundColor}");
            }
            else
            {
                bannerImage.color = new Color(0.96f, 0.93f, 0.90f, 1f); // #f5ece5
            }
            
            // Ajouter les coins arrondis si radius > 0
            if (config.frameRadius > 0)
            {
                var roundedCorners = bannerObj.AddComponent<RoundedCornersImage>();
                roundedCorners.cornerRadius = config.frameRadius;
                Debug.Log($"[SubtitleManager] ✅ Coins arrondis: radius={config.frameRadius}");
            }
        }
        else
        {
            if (ColorUtility.TryParseHtmlString(config.bottomBarColor, out Color bannerColor))
            {
                bannerColor.a = config.backgroundDimming;
                bannerImage.color = bannerColor;
            }
            else
            {
                bannerImage.color = new Color(0.96f, 0.93f, 0.90f, config.backgroundDimming);
            }
        }
        
        bannerImage.raycastTarget = false;
        
        // Positionner les textes selon le mode
        float padL = config.useFrameMode ? config.framePaddingLeft : config.paddingLeft;
        float padR = config.useFrameMode ? config.framePaddingRight : config.paddingRight;
        float padT = config.useFrameMode ? config.framePaddingTop : 0;
        float padB = config.useFrameMode ? config.framePaddingBottom : 0;
        
        // Padding intérieur supplémentaire pour le conteneur titre+sous-titre
        float tcPadL = config.useFrameMode ? config.textContentPaddingLeft : 0;
        float tcPadR = config.useFrameMode ? config.textContentPaddingRight : 0;
        float tcPadT = config.useFrameMode ? config.textContentPaddingTop : 0;
        float tcPadB = config.useFrameMode ? config.textContentPaddingBottom : 0;
        
        // Padding total = frame padding + text content padding
        float totalPadL = padL + tcPadL;
        float totalPadR = padR + tcPadR;
        float totalPadT = padT + tcPadT;
        float totalPadB = padB + tcPadB;
        
        Debug.Log($"[SubtitleManager] speakerText null? {speakerText == null}, subtitleText null? {subtitleText == null}");
        
        if (config.useFrameMode)
        {
            // En mode frame, créer les textes directement dans le cadre
            // Créer le texte du speaker
            GameObject speakerObj = new GameObject("FrameSpeakerText");
            speakerObj.transform.SetParent(bannerObj.transform, false);
            
            RectTransform speakerRect = speakerObj.AddComponent<RectTransform>();
            speakerRect.anchorMin = new Vector2(0, 1f);
            speakerRect.anchorMax = new Vector2(1, 1f);
            speakerRect.pivot = new Vector2(0f, 1f);
            speakerRect.anchoredPosition = new Vector2(totalPadL, -totalPadT);
            speakerRect.sizeDelta = new Vector2(-totalPadL - totalPadR, 50);
            
            var frameSpeakerText = speakerObj.AddComponent<TMPro.TextMeshProUGUI>();
            frameSpeakerText.fontSize = config.speakerTextSize;
            frameSpeakerText.fontStyle = config.speakerTextBold ? TMPro.FontStyles.Bold : TMPro.FontStyles.Normal;
            frameSpeakerText.alignment = TMPro.TextAlignmentOptions.TopLeft;
            if (ColorUtility.TryParseHtmlString(config.speakerTextColor, out Color spkColor))
                frameSpeakerText.color = spkColor;
            else
                frameSpeakerText.color = new Color(0.39f, 0.28f, 0.50f, 1f); // violet
            
            // Copier le texte existant si disponible
            if (speakerText != null && !string.IsNullOrEmpty(speakerText.text))
            {
                frameSpeakerText.text = speakerText.text;
            }
            
            // Remplacer la référence pour les futures mises à jour
            speakerText = frameSpeakerText;
            speakerObj.SetActive(true); // 🔧 Activer explicitement
            Debug.Log($"[SubtitleManager] ✅ Speaker text créé dans le cadre - parent: {speakerObj.transform.parent.name}");
            
            // Créer le texte du dialogue
            GameObject dialogueObj = new GameObject("FrameDialogueText");
            dialogueObj.transform.SetParent(bannerObj.transform, false);
            
            RectTransform dialogueRect = dialogueObj.AddComponent<RectTransform>();
            dialogueRect.anchorMin = new Vector2(0, 0f);
            dialogueRect.anchorMax = new Vector2(1, 1f);
            dialogueRect.offsetMin = new Vector2(totalPadL, totalPadB + 40); // +40 pour l'indicateur
            dialogueRect.offsetMax = new Vector2(-totalPadR, -totalPadT - 50 - config.speakerMarginBottom);
            
            var frameDialogueText = dialogueObj.AddComponent<TMPro.TextMeshProUGUI>();
            frameDialogueText.fontSize = config.dialogueTextSize;
            if (ColorUtility.TryParseHtmlString(config.dialogueTextColor, out Color dlgColor))
                frameDialogueText.color = dlgColor;
            else
                frameDialogueText.color = new Color(0.29f, 0.29f, 0.29f, 1f); // gris foncé
            
            // Alignement selon la config
            switch (config.dialogueTextAlignment?.ToLower())
            {
                case "center":
                    frameDialogueText.alignment = TMPro.TextAlignmentOptions.TopJustified;
                    break;
                case "right":
                    frameDialogueText.alignment = TMPro.TextAlignmentOptions.TopRight;
                    break;
                default:
                    frameDialogueText.alignment = TMPro.TextAlignmentOptions.TopLeft;
                    break;
            }
            
            // Copier le texte existant si disponible
            if (subtitleText != null && !string.IsNullOrEmpty(subtitleText.text))
            {
                frameDialogueText.text = subtitleText.text;
            }
            
            // Remplacer la référence pour les futures mises à jour
            subtitleText = frameDialogueText;
            dialogueObj.SetActive(true); // 🔧 Activer explicitement
            Debug.Log($"[SubtitleManager] ✅ Dialogue text créé dans le cadre");
            
            // Créer l'indicateur de continuation (image PNG au lieu de la flèche ▼)
            if (config.showContinueIndicator)
            {
                GameObject indicatorObj = new GameObject("ContinueIndicator");
                indicatorObj.transform.SetParent(bannerObj.transform, false);
                
                RectTransform indicatorRect = indicatorObj.AddComponent<RectTransform>();
                indicatorRect.anchorMin = new Vector2(0.5f, 0f);
                indicatorRect.anchorMax = new Vector2(0.5f, 0f);
                indicatorRect.pivot = new Vector2(0.5f, 0f);
                indicatorRect.anchoredPosition = new Vector2(0, config.continueIndicatorBottomMargin);
                indicatorRect.sizeDelta = new Vector2(145, 35); // Taille exacte de dialogue_next.png
                
                Debug.Log("[SubtitleManager] 🎨 Création de l'Image PNG pour l'indicateur");
                
                // Créer une Image au lieu d'un TextMeshProUGUI
                var indicatorImage = indicatorObj.AddComponent<UnityEngine.UI.Image>();
                indicatorImage.preserveAspect = true;
                indicatorImage.raycastTarget = false;
                indicatorImage.color = Color.white;
                
                // Charger l'image dialogue_next.png depuis uiPath
                if (GeneralConfigManager.Instance != null)
                {
                    string pngUrl = GeneralConfigManager.Instance.GetUIUrl("dialogue_next.png");
                    StartCoroutine(LoadContinueIndicatorImage(indicatorImage, pngUrl));
                    Debug.Log($"[SubtitleManager] 📥 Chargement de l'indicateur PNG depuis: {pngUrl}");
                }
                else
                {
                    Debug.LogError("[SubtitleManager] ❌ GeneralConfigManager.Instance est null!");
                }
            }
        }
        else
        {
            // Mode legacy : repositionner les textes existants
            bannerObj.transform.SetSiblingIndex(1);
            
            // 🔧 Fallback: si les refs sont null (cas observé dans les logs), recréer des TMP dans le banner.
            if (speakerText == null || subtitleText == null)
            {
                Debug.LogWarning("[SubtitleManager] Mode legacy mais speakerText/subtitleText sont NULL -> création de textes dans le banner (fallback)");

                GameObject speakerObj = new GameObject("LegacySpeakerText");
                speakerObj.transform.SetParent(bannerObj.transform, false);
                RectTransform speakerRect = speakerObj.AddComponent<RectTransform>();
                speakerRect.anchorMin = new Vector2(0, 1f);
                speakerRect.anchorMax = new Vector2(1, 1f);
                speakerRect.pivot = new Vector2(0f, 1f);
                speakerRect.anchoredPosition = new Vector2(padL, -10f);
                speakerRect.sizeDelta = new Vector2(-padL - padR, 50f);

                var legacySpeaker = speakerObj.AddComponent<TMPro.TextMeshProUGUI>();
                legacySpeaker.fontSize = config.speakerTextSize;
                legacySpeaker.fontStyle = config.speakerTextBold ? TMPro.FontStyles.Bold : TMPro.FontStyles.Normal;
                legacySpeaker.alignment = TMPro.TextAlignmentOptions.TopLeft;
                if (ColorUtility.TryParseHtmlString(config.speakerTextColor, out Color spkColor))
                    legacySpeaker.color = spkColor;

                GameObject dialogueObj = new GameObject("LegacyDialogueText");
                dialogueObj.transform.SetParent(bannerObj.transform, false);
                RectTransform dialogueRect = dialogueObj.AddComponent<RectTransform>();
                dialogueRect.anchorMin = new Vector2(0, 0f);
                dialogueRect.anchorMax = new Vector2(1, 1f);
                dialogueRect.offsetMin = new Vector2(padL, 10f);
                dialogueRect.offsetMax = new Vector2(-padR, -60f);

                var legacyDialogue = dialogueObj.AddComponent<TMPro.TextMeshProUGUI>();
                legacyDialogue.fontSize = config.dialogueTextSize;
                legacyDialogue.alignment = TMPro.TextAlignmentOptions.TopLeft;
                if (ColorUtility.TryParseHtmlString(config.dialogueTextColor, out Color dlgColor))
                    legacyDialogue.color = dlgColor;

                speakerText = legacySpeaker;
                subtitleText = legacyDialogue;

                speakerObj.SetActive(true);
                dialogueObj.SetActive(true);
            }

            if (speakerText != null) 
            {
                speakerText.transform.SetAsLastSibling();
                RectTransform speakerRect = speakerText.GetComponent<RectTransform>();
                if (speakerRect != null)
                {
                    speakerRect.offsetMin = new Vector2(padL, speakerRect.offsetMin.y);
                    speakerRect.offsetMax = new Vector2(-padR, speakerRect.offsetMax.y);
                }
            }
            
            if (subtitleText != null) 
            {
                subtitleText.transform.SetAsLastSibling();
                RectTransform subtitleRect = subtitleText.GetComponent<RectTransform>();
                if (subtitleRect != null)
                {
                    subtitleRect.offsetMin = new Vector2(padL, subtitleRect.offsetMin.y);
                    subtitleRect.offsetMax = new Vector2(-padR, subtitleRect.offsetMax.y);
                }
            }
        }
    }

    private UnityEngine.InputSystem.Key ParseKey(string keyName)
    {
        if (System.Enum.TryParse<UnityEngine.InputSystem.Key>(keyName, true, out var key))
        {
            return key;
        }
        
        switch (keyName.ToLower())
        {
            case "d": return UnityEngine.InputSystem.Key.D;
            case "q": return UnityEngine.InputSystem.Key.Q;
            case "escape": return UnityEngine.InputSystem.Key.Escape;
            case "space": return UnityEngine.InputSystem.Key.Space;
            default: return UnityEngine.InputSystem.Key.Space;
        }
    }
    
    /// <summary>
    /// Convertit DefaultDialogueConfig en DialogueConfig
    /// </summary>
    private DialogueConfig ConvertDefaultDialogueToConfig(DefaultDialogueConfig def)
    {
        if (def == null) return new DialogueConfig();
        
        return new DialogueConfig
        {
            // Paramètres de texte
            dialogueTextSize = def.dialogueTextSize > 0 ? def.dialogueTextSize : 28f,
            dialogueTextColor = !string.IsNullOrEmpty(def.dialogueTextColor) ? def.dialogueTextColor : "#4a4a4a",
            speakerTextSize = def.speakerTextSize > 0 ? def.speakerTextSize : 32f,
            speakerTextColor = !string.IsNullOrEmpty(def.speakerTextColor) ? def.speakerTextColor : "#64477f",
            speakerTextBold = def.speakerTextBold,
            speakerMarginBottom = def.speakerMarginBottom > 0 ? def.speakerMarginBottom : 15f,
            dialogueTextAlignment = !string.IsNullOrEmpty(def.dialogueTextAlignment) ? def.dialogueTextAlignment : "left",
            backgroundDimming = def.backgroundDimming,
            
            // Mode cadre centré
            useFrameMode = def.useFrameMode,
            frameWidth = def.frameWidth > 0 ? def.frameWidth : 1230f,
            frameHeight = def.frameHeight > 0 ? def.frameHeight : 340f,
            frameRadius = def.frameRadius > 0 ? def.frameRadius : 20f,
            frameBottomMargin = def.frameBottomMargin > 0 ? def.frameBottomMargin : 40f,
            frameBackgroundColor = !string.IsNullOrEmpty(def.frameBackgroundColor) ? def.frameBackgroundColor : "#f5ece5",
            framePaddingLeft = def.framePaddingLeft > 0 ? def.framePaddingLeft : 40f,
            framePaddingRight = def.framePaddingRight > 0 ? def.framePaddingRight : 40f,
            framePaddingTop = def.framePaddingTop > 0 ? def.framePaddingTop : 30f,
            framePaddingBottom = def.framePaddingBottom > 0 ? def.framePaddingBottom : 30f,
            
            // Padding intérieur du conteneur titre+sous-titre
            textContentPaddingLeft = def.textContentPaddingLeft,
            textContentPaddingRight = def.textContentPaddingRight,
            textContentPaddingTop = def.textContentPaddingTop,
            textContentPaddingBottom = def.textContentPaddingBottom,
            
            // Indicateur de continuation
            showContinueIndicator = def.showContinueIndicator,
            continueIndicatorColor = !string.IsNullOrEmpty(def.continueIndicatorColor) ? def.continueIndicatorColor : "#64477f",
            continueIndicatorSize = def.continueIndicatorSize > 0 ? def.continueIndicatorSize : 24f,
            continueIndicatorBottomMargin = def.continueIndicatorBottomMargin > 0 ? def.continueIndicatorBottomMargin : 20f,
            
            // Mode bandeau legacy
            bottomBarHeightRatio = def.bottomBarHeightRatio > 0 ? def.bottomBarHeightRatio : 0.32f,
            bottomBarColor = !string.IsNullOrEmpty(def.bottomBarColor) ? def.bottomBarColor : "#00000099",
            paddingLeft = def.paddingLeft,
            paddingRight = def.paddingRight,
            
            // Instructions
            instructionsText = !string.IsNullOrEmpty(def.instructionsText) ? def.instructionsText : "Cliquez pour continuer"
        };
    }

    // Coroutine pour charger l'image PNG de l'indicateur de continuation
    private IEnumerator LoadContinueIndicatorImage(UnityEngine.UI.Image indicatorImage, string url)
    {
        if (indicatorImage == null)
        {
            Debug.LogWarning("[SubtitleManager] indicatorImage est null, impossible de charger l'image");
            yield break;
        }
        
        string fullUrl = url;
        if (!url.StartsWith("http://") && !url.StartsWith("https://") && !url.StartsWith("file://"))
        {
            fullUrl = "file://" + System.IO.Path.GetFullPath(url);
        }
        
        using (UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequestTexture.GetTexture(fullUrl))
        {
            yield return request.SendWebRequest();
            
            if (request.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
            {
                Texture2D texture = UnityEngine.Networking.DownloadHandlerTexture.GetContent(request);
                Sprite sprite = Sprite.Create(
                    texture,
                    new Rect(0, 0, texture.width, texture.height),
                    new Vector2(0.5f, 0.5f)
                );
                indicatorImage.sprite = sprite;
                Debug.Log($"[SubtitleManager] ✅ Indicateur de continuation chargé: {url}");
            }
            else
            {
                Debug.LogError($"[SubtitleManager] ❌ Erreur lors du chargement de l'indicateur: {request.error}");
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // (Optionnel) bouton retour menu
    // ─────────────────────────────────────────────────────────────────────────────
    public void ReturnToMenu()
    {
#if UNITY_2021_3_OR_NEWER
        UnityEngine.SceneManagement.SceneManager.LoadScene("menu", UnityEngine.SceneManagement.LoadSceneMode.Single);
#endif
    }
}