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;

    [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;

    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_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 = 0f; overlayImage.color = c;
            overlayImage.preserveAspect = true;
        }

        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;
            }
        }
    }

    private void OnDisable()
    {
        CleanupVideoRT();
#if UNITY_WEBGL && !UNITY_EDITOR
        try { UI_SetVisible(0); } catch {}
        try { UI_HideOverlayImage((int)(overlayFadeInDuration * 1000f)); } catch {}
        try { WVO_Dispose(); } catch {}
#endif
    }

    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}");
        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)
    {
        using (var www = UnityWebRequest.Get(url))
        {
            yield return www.SendWebRequest();
            
            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();
            }
        }
    }

    // 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
    
    private void Update()
    {
        // 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;
                }
            }
        }
        
        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");
        
        // 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 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;
        }
        
        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
        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
        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}");
        }
        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}");
        }
        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
                if (GeneralConfigManager.Instance != null && GeneralConfigManager.Instance.IsConfigLoaded())
                {
                    var defConfig = GeneralConfigManager.Instance.GetDefaultDialogueConfig();
                    if (defConfig?.illustrationPosition != null)
                    {
                        posX = defConfig.illustrationPosition.x;
                        posY = defConfig.illustrationPosition.y;
                    }
                }
                UI_SetOverlayImagePosition(posX, posY);
                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;
        }

        StartCoroutine(LoadOverlaySprite_Desktop(imageNameOrUrl));
    }

    private IEnumerator LoadOverlaySprite_Desktop(string imageNameOrUrl)
    {
        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 (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)
        {
            using (var req = UnityWebRequestTexture.GetTexture(url))
            {
                yield return req.SendWebRequest();
#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;
        }

        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);
    }
    
    /// <summary>
    /// Positionne l'image overlay selon la configuration de general-config.json
    /// </summary>
    private void PositionOverlayImage()
    {
        if (overlayImage == null) return;
        
        // S'assurer que l'image a un Canvas override pour s'afficher AU-DESSUS du cadre des sous-titres (sortingOrder 1000)
        Canvas overlayCanvas = overlayImage.GetComponent<Canvas>();
        if (overlayCanvas == null)
        {
            overlayCanvas = overlayImage.gameObject.AddComponent<Canvas>();
            overlayCanvas.overrideSorting = true;
            overlayCanvas.sortingOrder = 1100; // Au-dessus du cadre de dialogue (1000)
            overlayImage.gameObject.AddComponent<UnityEngine.UI.GraphicRaycaster>();
            Debug.Log("[SubtitleManager] ✅ Canvas override ajouté à overlayImage (sortingOrder=1100)");
        }
        
        // Valeurs par défaut
        float posX = 1465f;
        float posY = 670f;
        
        // 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;
            }
        }
        
        // Conversion : position par rapport au centre du canvas (960, 540 en 1920x1080)
        float anchoredX = posX - 960f;
        float anchoredY = posY - 540f;
        
        RectTransform imgRect = 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);
        
        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) { var c = overlayImage.color; c.a = Mathf.Clamp01(a); overlayImage.color = c; }

    private IEnumerator FadeOverlayAlpha(float from, float to, float duration)
    {
        if (!overlayImage) yield break;
        float t = 0f; var c = overlayImage.color; c.a = from; overlayImage.color = c;
        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()
    {
        if (dialogueConfigApplied) 
        {
            Debug.Log("[SubtitleManager] Config déjà appliquée, skip");
            return;
        }
        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) 
        {
            Debug.LogWarning("[SubtitleManager] ⚠️ Aucune config disponible");
            return;
        }
        
        // 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 = FindFirstObjectByType<Canvas>();
        if (canvas == null)
        {
            Debug.LogWarning("[SubtitleManager] Aucun Canvas trouvé pour le bandeau");
            return;
        }
        
        GameObject oldBanner = GameObject.Find("DialogueBottomBanner");
        if (oldBanner != null)
        {
            Destroy(oldBanner);
        }
        
        GameObject bannerObj = new GameObject("DialogueBottomBanner");
        bannerObj.transform.SetParent(canvas.transform, false);
        
        // 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>();
        
        // 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;
            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;
            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);
            
            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
    }
}