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);
#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");
            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");
            OnDialogueComplete?.Invoke();
            return;
        }
        
        Debug.Log($"[SubtitleManager] Parsing dialogue depuis JSON string");
        ParseAndLoadDialogue(jsonString);
    }
    
    private void ParseAndLoadDialogue(string json)
    {
        try
        {
            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);
                
                project = new Project 
                { 
                    segments = new List<Segment> { segment },
                    videoRoot = "https://unjoursansassurance.studioplc.tech/demo_assets/videos/",
                    imageRoot = "https://unjoursansassurance.studioplc.tech/demo_assets/images/"
                };
                
                segIndex = 0;
                lineIndex = 0;
                LoadSegment(segIndex);
                ApplyCurrentLine();
            }
            else
            {
                Debug.LogWarning("[SubtitleManager] Aucune ligne de dialogue trouvée");
                OnDialogueComplete?.Invoke();
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"[SubtitleManager] Erreur parsing JSON dialogue: {e.Message}");
            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}");
                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);
                    
                    project = new Project 
                    { 
                        segments = new List<Segment> { segment },
                        videoRoot = "https://unjoursansassurance.studioplc.tech/demo_assets/videos/",
                        imageRoot = "https://unjoursansassurance.studioplc.tech/demo_assets/images/"
                    };
                    
                    segIndex = 0;
                    lineIndex = 0;
                    LoadSegment(segIndex);
                    ApplyCurrentLine();
                }
                else
                {
                    Debug.LogWarning("[SubtitleManager] Dialogue vide ou invalide");
                    OnDialogueComplete?.Invoke();
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"[SubtitleManager] Erreur parsing dialogue: {e.Message}");
                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 void Update()
    {
        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)      Next();
        else if (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");
            OnDialogueComplete?.Invoke();
        }
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // Navigation
    // ─────────────────────────────────────────────────────────────────────────────
    private void Next()
    {
        var seg = CurrentSeg(); 
        if (seg == null || seg.lines == null || seg.lines.Count == 0) return;
        
        lineIndex++;
        if (lineIndex >= seg.lines.Count)
        {
            // DIALOGUE TERMINÉ
            Debug.Log("[SubtitleManager] Dialogue terminé - déclenchement callback");
            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;
        }
    }

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

#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
        if (speakerText)  speakerText.text  = string.IsNullOrEmpty(seg.speaker) ? "" : seg.speaker;
        if (subtitleText) subtitleText.text = line.text ?? "";
#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);
            } 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;
                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;
        if (!wasVisible)
        {
            if (overlayFadeCoroutine != null) StopCoroutine(overlayFadeCoroutine);
            overlayFadeCoroutine = StartCoroutine(FadeOverlayAlpha(0f, 1f, overlayFadeInDuration));
            TrySpawnSparkleFXAtOverlay();
        }
        else SetOverlayAlpha(1f);
    }

    // ─────────────────────────────────────────────────────────────────────────────
    // 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
    // ─────────────────────────────────────────────────────────────────────────────
    public void ApplyDialogueConfig(DialogueConfig config)
    {
        if (config == null) return;
        
        Debug.Log("[SubtitleManager] Application de la configuration dialogue");
        
        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);
        
        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);
        
        RectTransform bannerRect = bannerObj.AddComponent<RectTransform>();
        bannerRect.anchorMin = new Vector2(0f, 0f);
        bannerRect.anchorMax = new Vector2(1f, config.bottomBarHeightRatio);
        bannerRect.offsetMin = Vector2.zero;
        bannerRect.offsetMax = Vector2.zero;
        
        Image bannerImage = bannerObj.AddComponent<Image>();
        
        if (ColorUtility.TryParseHtmlString(config.bottomBarColor, out Color bannerColor))
        {
            bannerColor.a = config.backgroundDimming;
            bannerImage.color = bannerColor;
            Debug.Log($"[SubtitleManager] Bandeau créé - Couleur: {config.bottomBarColor}, Alpha: {config.backgroundDimming}");
        }
        else
        {
            bannerImage.color = new Color(0.96f, 0.93f, 0.90f, config.backgroundDimming);
            Debug.LogWarning($"[SubtitleManager] Couleur bandeau invalide '{config.bottomBarColor}', utilisation couleur par défaut");
        }
        
        bannerImage.raycastTarget = false;
        bannerObj.transform.SetSiblingIndex(1);
        
        if (speakerText != null) 
        {
            speakerText.transform.SetAsLastSibling();
            RectTransform speakerRect = speakerText.GetComponent<RectTransform>();
            if (speakerRect != null)
            {
                speakerRect.offsetMin = new Vector2(config.paddingLeft, speakerRect.offsetMin.y);
                speakerRect.offsetMax = new Vector2(-config.paddingRight, speakerRect.offsetMax.y);
            }
        }
        
        if (subtitleText != null) 
        {
            subtitleText.transform.SetAsLastSibling();
            RectTransform subtitleRect = subtitleText.GetComponent<RectTransform>();
            if (subtitleRect != null)
            {
                subtitleRect.offsetMin = new Vector2(config.paddingLeft, subtitleRect.offsetMin.y);
                subtitleRect.offsetMax = new Vector2(-config.paddingRight, 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;
        }
    }

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