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


#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif

// ─────────────────────────────────────────────────────────────────────────────
// Modèles JSON (UNE SEULE FOIS)
// ─────────────────────────────────────────────────────────────────────────────
[Serializable]
public class DialogueConfig
{
    public string skipDialogueKey = "Escape";
    public string nextKey = "Space";
    public string prevKey = "Backspace";

    public bool  requireInputToAdvance = true;
    public float autoAdvanceDelay = 0f;
    public bool  showSkipInstructions = true;
    public float fadeTransitionDuration = 0.35f;

    public float  dialogueTextSize = 28f;
    public string dialogueTextColor = "#4a4a4a";
    public float  speakerTextSize = 32f;
    public string speakerTextColor = "#64477f";
    public bool   speakerTextBold = true;
    public float  speakerMarginBottom = 15f;
    public float  backgroundDimming = 0.25f;
    public string dialogueTextAlignment = "left";

    // Mode cadre centré (nouveau layout)
    public bool   useFrameMode = true;
    public float  frameWidth = 1230f;
    public float  frameHeight = 340f;
    public float  frameRadius = 20f;
    public float  frameBottomMargin = 40f;
    public string frameBackgroundColor = "#f5ece5";
    public float  framePaddingLeft = 40f;
    public float  framePaddingRight = 40f;
    public float  framePaddingTop = 30f;
    public float  framePaddingBottom = 30f;
    
    // Padding intérieur du conteneur titre+sous-titre
    public float  textContentPaddingLeft = 0f;
    public float  textContentPaddingRight = 0f;
    public float  textContentPaddingTop = 0f;
    public float  textContentPaddingBottom = 0f;

    // Indicateur de continuation (flèche en bas)
    public bool   showContinueIndicator = true;
    public string continueIndicatorColor = "#64477f";
    public float  continueIndicatorSize = 24f;
    public float  continueIndicatorBottomMargin = 20f;

    // Contrôle du bandeau bas (mode legacy)
    public float  bottomBarHeightRatio = 0.32f;     // 0..1
    public string bottomBarColor = "#00000099";     // noir ~60% alpha
    public float  paddingLeft = 24f;                // px
    public float  paddingRight = 24f;               // px
    
    // Texte des instructions de navigation
    public string instructionsText = "Cliquez pour continuer";
    
    // Position de l'image d'illustration (coordonnées en 1920x1080) - DÉPRÉCIÉ
    public IllustrationPositionConfig illustrationPosition;
    // Nouvelle méthode de positionnement (relatif au panneau)
    public IllustrationOffsetFromPanelConfig illustrationOffsetFromPanel;

    // Taille / style de l'illustration (avatar rond)
    // Diamètre unique (prioritaire si > 0)
    public float illustrationDiameter = 160f;
    // Ancien format (x/y) conservé pour rétro-compatibilité
    public IllustrationSizeConfig illustrationSize;
    public IllustrationBorderConfig illustrationBorder;
    public string illustrationBackgroundColor = "#FF0000";
    public IllustrationGradientConfig illustrationBackgroundGradient;
}

[Serializable]
public class DialogueLine
{
    public string text;
    public string speaker;      // si vide -> on prendra DialogueSequence.speaker
    public string image;        // illustration par ligne (chemin relatif à imageRoot)
    public float  duration = 0; // auto-advance de la ligne (0 = désactivé)
    public float  delay = 0;    // délai AVANT d’afficher la ligne
}

[Serializable]
public class DialogueSequence
{
    public string id;
    public string title;
    public string speaker;     // orateur par défaut (hérite sur les lignes vides)
    public string video;       // nom du fichier vidéo de fond (utilisé par le GameManager)
    public List<DialogueLine> lines;
}

[Serializable]
public class DialogueWrapper
{
    public string videoRoot;              // racine vidéo (optionnel, sinon global)
    public string imageRoot;              // racine images (optionnel, sinon global)
    public DialogueConfig dialogueConfig; // config spécifique à ce dialogue (optionnel)
    public DialogueSequence dialogue;
}

/// <summary>
/// Composant Unity pour jouer les séquences de dialogue
/// </summary>
public class DialoguePlayer : MonoBehaviour
{
    [Header("Illustration (optionnel)")]
    public Image illustrationImage;             // assigner un Image dans le Canvas
    public float illustrationFadeDuration = 0.25f;

    private CanvasGroup _illustrationCg;
    private GameObject _illustrationContainer;
    private AspectRatioFitter _illustrationAspectFitter;
    private RectTransform _illustrationInnerMaskRect;
    private CircleMaskGraphic _illustrationBorderGraphic;
    private CircleMaskGraphic _illustrationMaskGraphic; // sert de forme de mask
    private Image _illustrationBgImage;
    private float _illustrationBorderWidthPx = 4f;
    private Texture2D _illustrationBgTex;
    private Sprite _illustrationBgSprite;
    private float _illustrationBgDiameterCached = -1f;
    private string _imageRoot = null;
    private string _currentImageUrl = null;
    
    // Listes de tracking pour cleanup des ressources d'illustration
    private List<Texture2D> dynamicIllustrationTextures = new List<Texture2D>();
    private List<Sprite> dynamicIllustrationSprites = new List<Sprite>();

    // Dernières ressources d'illustration (pour cleanup et éviter les fuites)
    private Sprite _currentIllustrationSprite;
    private Texture2D _currentIllustrationTexture;

    [Header("Background (optionnel)")]
    // RawImage plein écran pour afficher la RenderTexture vidéo fournie par le GameManager
    public RawImage videoBackgroundUI;

    [Header("UI References")]
    public GameObject dialoguePanel;
    public TextMeshProUGUI dialogueText;
    public TextMeshProUGUI speakerText;
    public TextMeshProUGUI skipInstructionText; // Legacy mode uniquement
    public Image continueIndicatorImage; // Mode frame : image PNG pour l'indicateur
    public Image backgroundImage; // voile noir (dimming)
    public Image overlayImage;    // fallback si tu n'utilises pas illustrationImage

    // Bandeau bas (auto-créé) — références pour appliquer la config
    private RectTransform bottomBarRect;
    private Image bottomBarBg;
    private RoundedCornersImage bottomBarRoundedCorners; // Pour les coins arrondis du cadre
    private RectTransform speakerRectRt, dialogueRectRt, instructionsRectRt;

    [Header("Auto-Creation Settings")]
    public bool autoCreateUI = true;

    // États et configuration
    private DialogueSequence currentDialogue;
    private DialogueConfig currentConfig;
    private int currentLineIndex = 0;
    private bool isPlaying = false;
    private bool isWaitingForInput = false;
    private Coroutine dialogueCoroutine;
    private Coroutine _playFromUrlCoroutine;
    private Coroutine _continueIndicatorLoadCoroutine;
    private int _playToken = 0; // tracker: invalide les coroutines "en vol" lors d'un Stop/Cleanup
    private bool _cleanupRegistered = false;

    // Callbacks
    public Action OnDialogueComplete;
    public Action OnDialogueSkipped;
    public Action<DialogueLine> OnLineDisplayed;

    // Configuration par défaut (chargée depuis GeneralConfigManager si disponible)
    private DialogueConfig defaultConfig = new DialogueConfig();
    private bool configLoadedFromManager = false;

    // --- Vidéo interne du dialogue ---
    private VideoPlayer _bgVideoPlayer;
    private RenderTexture _bgRT;
    private string _videoRoot = null;   // peut venir du JSON, sinon reste null

    // Appelé si tu veux forcer un root vidéo global depuis l’extérieur
    public void SetVideoRoot(string root) { _videoRoot = root; }

    private Coroutine _overlayLoadCo = null;

#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
    private struct MacHiddenImageState
    {
        public Image image;
        public Color color;
        public bool enabled;
        public bool raycastTarget;
    }

    private readonly List<MacHiddenImageState> _macHiddenImages = new List<MacHiddenImageState>();
#endif

// Tente de retrouver les éléments du bas si l'UI n'a pas été auto-créée par le script
private void EnsureBottomBarRefs()
{
    if (dialoguePanel == null) return;

    Debug.Log($"[DialoguePlayer] EnsureBottomBarRefs - Recherche dans: {dialoguePanel.name}");
    
    // Chercher le BottomBar avec plusieurs noms possibles
    if (bottomBarRect == null)
    {
        var bar = dialoguePanel.transform.Find("BottomBar");
        if (bar == null) bar = dialoguePanel.transform.Find("SubtitleBar");
        if (bar == null) bar = dialoguePanel.transform.Find("TextArea");
        if (bar == null) bar = dialoguePanel.transform.Find("DialogueBar");
        
        // Si toujours pas trouvé, chercher le premier enfant avec un RectTransform et Image
        if (bar == null)
        {
            foreach (Transform child in dialoguePanel.transform)
            {
                if (child.GetComponent<Image>() != null && child.name.ToLower().Contains("bar") || 
                    child.name.ToLower().Contains("bottom") || child.name.ToLower().Contains("subtitle"))
                {
                    bar = child;
                    break;
                }
            }
        }
        
        if (bar != null)
        {
            bottomBarRect = bar.GetComponent<RectTransform>();
            bottomBarBg = bar.GetComponent<Image>();
            Debug.Log($"[DialoguePlayer] ✅ BottomBar trouvé: {bar.name}");
        }
        else
        {
            Debug.LogWarning("[DialoguePlayer] ⚠️ BottomBar non trouvé!");
            // Lister les enfants pour debug
            Debug.Log($"[DialoguePlayer] Enfants de {dialoguePanel.name}:");
            foreach (Transform child in dialoguePanel.transform)
            {
                Debug.Log($"  - {child.name}");
            }
        }
    }

    // Chercher SpeakerText
    if (speakerRectRt == null && bottomBarRect != null)
    {
        var t = bottomBarRect.transform.Find("SpeakerText");
        if (t == null) t = bottomBarRect.transform.Find("Speaker");
        if (t == null) t = bottomBarRect.transform.Find("Title");
        if (t == null) t = bottomBarRect.transform.Find("Name");
        
        if (t != null)
        {
            speakerRectRt = t.GetComponent<RectTransform>();
            if (speakerText == null) speakerText = t.GetComponent<TextMeshProUGUI>();
            Debug.Log($"[DialoguePlayer] ✅ SpeakerText trouvé: {t.name}");
        }
    }

    // Chercher DialogueText
    if (dialogueRectRt == null && bottomBarRect != null)
    {
        var t = bottomBarRect.transform.Find("DialogueText");
        if (t == null) t = bottomBarRect.transform.Find("Text");
        if (t == null) t = bottomBarRect.transform.Find("Subtitle");
        if (t == null) t = bottomBarRect.transform.Find("Content");
        
        if (t != null)
        {
            dialogueRectRt = t.GetComponent<RectTransform>();
            if (dialogueText == null) dialogueText = t.GetComponent<TextMeshProUGUI>();
            Debug.Log($"[DialoguePlayer] ✅ DialogueText trouvé: {t.name}");
        }
    }

    // Chercher Instructions
    if (instructionsRectRt == null && bottomBarRect != null)
    {
        var t = bottomBarRect.transform.Find("Instructions");
        if (t == null) t = bottomBarRect.transform.Find("Skip");
        if (t == null) t = bottomBarRect.transform.Find("Continue");
        
        if (t != null)
        {
            instructionsRectRt = t.GetComponent<RectTransform>();
            if (skipInstructionText == null) skipInstructionText = t.GetComponent<TextMeshProUGUI>();
        }
    }
    
    Debug.Log($"[DialoguePlayer] Refs: bottomBarRect={(bottomBarRect != null)}, bottomBarBg={(bottomBarBg != null)}, speakerText={(speakerText != null)}, dialogueText={(dialogueText != null)}");
}


   

    private static bool IsAbsoluteUrl(string u)
{
    if (string.IsNullOrEmpty(u)) return false;
    return Uri.TryCreate(u, UriKind.Absolute, out _);
}

    // ─────────────────────────────────────────────────────────────────────────
    // Lifecycle
    // ─────────────────────────────────────────────────────────────────────────
    void Awake()
    {
        // Ne pas créer l'UI immédiatement, attendre Start() pour avoir la config
        Debug.Log($"[DialoguePlayer] Awake - illustrationImage assignée? {illustrationImage != null}");

        // Si une illustrationImage est assignée dans l'inspector, on l'encapsule dans un conteneur rond.
        // Sinon, elle sera créée dynamiquement dans InitializeWithConfig.
        if (illustrationImage != null)
        {
            SetupIllustrationCircleContainer(illustrationImage.transform.parent, illustrationImage);
        }

        if (overlayImage != null)
        {
            overlayImage.sprite = null;
            overlayImage.enabled = false;            // <- pas de quad blanc
            overlayImage.gameObject.SetActive(false);
        }


        if (videoBackgroundUI != null)
        {
            videoBackgroundUI.texture = null;
            videoBackgroundUI.gameObject.SetActive(false);
            videoBackgroundUI.enabled = false;
        }

        DisableEmptyUIRenderers();

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

    private void OnDestroy()
    {
        // Ne pas appeler CleanupPoints.Instance ici (ça pourrait recréer le singleton pendant le teardown).
        try
        {
            var cp = CleanupPoints.GetExisting();
            if (cp != null) cp.UnregisterOnSceneChange("DialoguePlayer");
        }
        catch { /* ignore */ }
    }

    private void SafeCleanupForSceneChange()
    {
        // Invalider toute coroutine en vol
        _playToken++;

        // Stopper toutes les coroutines (downloads, fades, vidéo, etc.)
        try { StopAllCoroutines(); } catch { /* ignore */ }
        dialogueCoroutine = null;
        _overlayLoadCo = null;
        _playFromUrlCoroutine = null;
        _continueIndicatorLoadCoroutine = null;

        // Nettoyage visuel + ressources (RT, video, sprites)
        try { ForceCleanupAndHide(); } catch { /* ignore */ }
    }

    void Start()
    {
        StartCoroutine(InitializeWithConfig());
    }
    
    /// <summary>
    /// Charge la configuration depuis GeneralConfigManager puis crée l'UI
    /// </summary>
    private IEnumerator InitializeWithConfig()
    {
        // Attendre que GeneralConfigManager soit disponible et chargé
        float timeout = 5f;
        float elapsed = 0f;
        
        while (GeneralConfigManager.Instance == null || !GeneralConfigManager.Instance.IsConfigLoaded())
        {
            elapsed += Time.deltaTime;
            if (elapsed > timeout)
            {
                Debug.LogWarning("[DialoguePlayer] Timeout en attendant GeneralConfigManager - utilisation config par défaut");
                break;
            }
            yield return null;
        }
        
        // Charger la configuration depuis GeneralConfigManager
        LoadConfigFromManager();
        
        // Créer l'UI avec la bonne configuration
        if (autoCreateUI && dialoguePanel == null)
        {
            CreateDialogueUI();
        }
        
        // S'assurer que illustrationImage existe même si dialoguePanel était déjà là
        if (illustrationImage == null && dialoguePanel != null)
        {
            CreateIllustrationImage();
        }
        
        if (dialoguePanel != null)
            dialoguePanel.SetActive(false);
    }
    
    /// <summary>
    /// Crée l'image d'illustration si elle n'existe pas
    /// </summary>
    private void CreateIllustrationImage()
    {
        if (illustrationImage != null) return;
        
        // Trouver le parent approprié (dialoguePanel ou son Canvas parent)
        Transform parent = dialoguePanel?.transform;
        if (parent == null)
        {
            Debug.LogWarning("[DialoguePlayer] Impossible de créer illustrationImage: pas de parent disponible");
            return;
        }

        // Créer un conteneur circulaire avec fond rouge + mask, puis une Image enfant pour l'illustration.
        SetupIllustrationCircleContainer(parent, null);

        // Appliquer position + taille + style depuis la config (general-config.json)
        ApplyIllustrationLayout(currentConfig ?? defaultConfig);
        Debug.Log("[DialoguePlayer] ✅ IllustrationCircle créée (panel existant) avec layout depuis config");
    }

    private void ApplyIllustrationLayout(DialogueConfig cfg)
    {
        if (_illustrationContainer == null) return;

        RectTransform containerRect = _illustrationContainer.GetComponent<RectTransform>();
        
        Debug.Log($"[DialoguePlayer] 🎨 ApplyIllustrationLayout appelé - illustrationOffsetFromPanel null? {cfg?.illustrationOffsetFromPanel == null}");
        
        // Toujours utiliser le système absolu pour éviter les problèmes
        float posX = cfg?.illustrationPosition?.x ?? 1465f;
        float posY = cfg?.illustrationPosition?.y ?? 670f;
        float anchoredX = posX - 960f;
        float anchoredY = posY - 540f;
        
        containerRect.anchorMin = new Vector2(0.5f, 0.5f);
        containerRect.anchorMax = new Vector2(0.5f, 0.5f);
        containerRect.pivot = new Vector2(0.5f, 0.5f);
        containerRect.anchoredPosition = new Vector2(anchoredX, anchoredY);
        
        Debug.Log($"[DialoguePlayer] 🎨 IllustrationContainer positionnée à ({posX}, {posY}) -> anchoredPosition ({anchoredX}, {anchoredY})");

        // Taille: diamètre unique prioritaire
        float diameter = (cfg != null && cfg.illustrationDiameter > 0f) ? cfg.illustrationDiameter : 0f;
        if (diameter <= 0f && cfg?.illustrationSize != null)
        {
            // rétro-compat: prendre X ou Y si défini
            diameter = cfg.illustrationSize.x > 0f ? cfg.illustrationSize.x : cfg.illustrationSize.y;
        }
        if (diameter <= 0f) diameter = 160f;
        containerRect.sizeDelta = new Vector2(diameter, diameter);

        float bw = (cfg?.illustrationBorder != null && cfg.illustrationBorder.width > 0f) ? cfg.illustrationBorder.width : 4f;
        _illustrationBorderWidthPx = bw;

        Color borderCol = HexToColorSafe(cfg?.illustrationBorder?.color, new Color(0.956f, 0.925f, 0.898f, 1f)); // #F4ECE5 fallback
        Color bgCol = HexToColorSafe(cfg?.illustrationBackgroundColor, Color.red); // fallback si pas de gradient

        if (_illustrationBorderGraphic != null) _illustrationBorderGraphic.color = borderCol;

        // Fond: gradient si configuré, sinon couleur unie
        bool hasGradient = (cfg?.illustrationBackgroundGradient != null &&
                            !string.IsNullOrEmpty(cfg.illustrationBackgroundGradient.startColor) &&
                            !string.IsNullOrEmpty(cfg.illustrationBackgroundGradient.endColor));
        if (_illustrationBgImage != null)
        {
            if (hasGradient)
            {
                Color top = HexToColorSafe(cfg.illustrationBackgroundGradient.startColor, new Color(0.533f, 0.541f, 0.871f, 1f));    // ~#888ADE
                Color bottom = HexToColorSafe(cfg.illustrationBackgroundGradient.endColor, new Color(0.235f, 0.255f, 0.357f, 1f)); // ~#3C415B
                EnsureIllustrationGradientSprite(diameter, top, bottom);
                _illustrationBgImage.sprite = _illustrationBgSprite;
                _illustrationBgImage.color = Color.white;
            }
            else
            {
                _illustrationBgImage.sprite = null;
                _illustrationBgImage.color = bgCol;
            }
        }
        if (_illustrationInnerMaskRect != null)
        {
            _illustrationInnerMaskRect.offsetMin = new Vector2(_illustrationBorderWidthPx, _illustrationBorderWidthPx);
            _illustrationInnerMaskRect.offsetMax = new Vector2(-_illustrationBorderWidthPx, -_illustrationBorderWidthPx);
        }
    }

    private void EnsureIllustrationGradientSprite(float diameter, Color top, Color bottom)
    {
        int d = Mathf.Max(8, Mathf.RoundToInt(diameter));
        if (_illustrationBgSprite != null && Mathf.Approximately(_illustrationBgDiameterCached, d)) return;

        // Détruire ancien (évite fuites WebGL)
        if (_illustrationBgSprite != null) { Destroy(_illustrationBgSprite); _illustrationBgSprite = null; }
        if (_illustrationBgTex != null) { Destroy(_illustrationBgTex); _illustrationBgTex = null; }

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

        Color[] pixels = new Color[d * d];
        for (int y = 0; y < d; y++)
        {
            float t = d <= 1 ? 0f : (float)y / (d - 1);
            Color c = Color.Lerp(bottom, top, t); // haut -> bas (180deg)
            for (int x = 0; x < d; x++)
                pixels[y * d + x] = c;
        }
        _illustrationBgTex.SetPixels(pixels);
        _illustrationBgTex.Apply();

        _illustrationBgSprite = Sprite.Create(_illustrationBgTex, new Rect(0, 0, d, d), new Vector2(0.5f, 0.5f), 100f);
        dynamicIllustrationTextures.Add(_illustrationBgTex);
        dynamicIllustrationSprites.Add(_illustrationBgSprite);
    }

    private Color HexToColorSafe(string hex, Color fallback)
    {
        if (string.IsNullOrEmpty(hex)) return fallback;
        if (ColorUtility.TryParseHtmlString(hex, out Color c)) return c;
        if (!hex.StartsWith("#") && ColorUtility.TryParseHtmlString("#" + hex, out c)) return c;
        return fallback;
    }

    private void SetupIllustrationCircleContainer(Transform parent, Image existingIllustrationImage)
    {
        if (_illustrationContainer != null) return;

        if (parent == null)
        {
            Debug.LogWarning("[DialoguePlayer] SetupIllustrationCircleContainer: parent null");
            return;
        }

        _illustrationContainer = new GameObject("IllustrationCircle");
        _illustrationContainer.transform.SetParent(parent, false);
        _illustrationContainer.transform.SetAsLastSibling();

        // Canvas override pour s'afficher PAR-DESSUS le cadre des sous-titres (sortingOrder 1000)
        Canvas illustrationCanvas = _illustrationContainer.AddComponent<Canvas>();
        illustrationCanvas.overrideSorting = true;
        illustrationCanvas.sortingOrder = 1100;
        _illustrationContainer.AddComponent<UnityEngine.UI.GraphicRaycaster>();

        // GetComponent au lieu de AddComponent (RectTransform est ajouté automatiquement)
        RectTransform containerRect = _illustrationContainer.GetComponent<RectTransform>();
        containerRect.anchorMin = new Vector2(0.5f, 0.5f);
        containerRect.anchorMax = new Vector2(0.5f, 0.5f);
        containerRect.pivot = new Vector2(0.5f, 0.5f);
        containerRect.sizeDelta = new Vector2(160, 160);

        // CanvasGroup sur le conteneur pour le fade
        _illustrationCg = _illustrationContainer.GetComponent<CanvasGroup>();
        if (_illustrationCg == null) _illustrationCg = _illustrationContainer.AddComponent<CanvasGroup>();
        _illustrationCg.alpha = 0f;

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

        // 2) Cercle intérieur (fond + masque)
        GameObject maskObj = new GameObject("IllustrationMaskCircle");
        maskObj.transform.SetParent(_illustrationContainer.transform, false);
        _illustrationInnerMaskRect = maskObj.AddComponent<RectTransform>();
        _illustrationInnerMaskRect.anchorMin = Vector2.zero;
        _illustrationInnerMaskRect.anchorMax = Vector2.one;
        _illustrationInnerMaskRect.offsetMin = new Vector2(_illustrationBorderWidthPx, _illustrationBorderWidthPx);
        _illustrationInnerMaskRect.offsetMax = new Vector2(-_illustrationBorderWidthPx, -_illustrationBorderWidthPx);

        _illustrationMaskGraphic = maskObj.AddComponent<CircleMaskGraphic>();
        _illustrationMaskGraphic.color = Color.white; // forme du mask (invisible si showMaskGraphic=false)
        _illustrationMaskGraphic.raycastTarget = false;

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

        // Fond (dans le mask) : couleur unie par défaut, ou sprite gradient via ApplyIllustrationLayout
        GameObject bgObj = new GameObject("Background");
        bgObj.transform.SetParent(maskObj.transform, false);
        RectTransform bgRect = bgObj.AddComponent<RectTransform>();
        bgRect.anchorMin = Vector2.zero;
        bgRect.anchorMax = Vector2.one;
        bgRect.offsetMin = Vector2.zero;
        bgRect.offsetMax = Vector2.zero;
        _illustrationBgImage = bgObj.AddComponent<Image>();
        _illustrationBgImage.raycastTarget = false;
        _illustrationBgImage.color = Color.red;

        // 2. IMAGE AVATAR (par-dessus)
        Image img;
        if (existingIllustrationImage != null)
        {
            existingIllustrationImage.transform.SetParent(maskObj.transform, false);
            img = existingIllustrationImage;
        }
        else
        {
            GameObject imgObj = new GameObject("IllustrationImage");
            imgObj.transform.SetParent(maskObj.transform, false);
            img = imgObj.AddComponent<Image>();
        }

        illustrationImage = img;
        illustrationImage.sprite = null; // Pas de sprite par défaut
        illustrationImage.color = Color.white; // Couleur blanche pour l'avatar
        illustrationImage.type = Image.Type.Simple;
        illustrationImage.preserveAspect = true;
        illustrationImage.raycastTarget = false;
        illustrationImage.enabled = false;

        RectTransform imgRect = illustrationImage.rectTransform;
        imgRect.anchorMin = Vector2.zero;
        imgRect.anchorMax = Vector2.one;
        imgRect.sizeDelta = Vector2.zero;

        _illustrationContainer.SetActive(false);
        
        Debug.Log("[DialoguePlayer] ✅ IllustrationCircle créée : bordure + mask rond + fond + image");
    }
    
    /// <summary>
    /// Crée un sprite circulaire (inspiré de CreateRoundedSprite de QuetesTab)
    /// </summary>
    private Sprite CreateCircleSprite(int size, float radius)
    {
        Texture2D tex = new Texture2D(size, size, TextureFormat.RGBA32, false);
        Color[] pixels = new Color[size * size];
        
        float centerX = size / 2f;
        float centerY = size / 2f;
        
        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                float dx = x - centerX;
                float dy = y - centerY;
                float dist = Mathf.Sqrt(dx * dx + dy * dy);
                
                // Anti-aliasing sur 1.5px au bord
                if (dist <= radius - 1.5f)
                {
                    pixels[y * size + x] = Color.white;
                }
                else if (dist <= radius)
                {
                    float alpha = 1f - (dist - (radius - 1.5f)) / 1.5f;
                    pixels[y * size + x] = new Color(1, 1, 1, alpha);
                }
                else
                {
                    pixels[y * size + x] = Color.clear;
                }
            }
        }
        
        tex.SetPixels(pixels);
        tex.Apply();
        
        Sprite sprite = Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f);
        
        // Tracker pour cleanup
        dynamicIllustrationTextures.Add(tex);
        dynamicIllustrationSprites.Add(sprite);
        
        return sprite;
    }
    
    /// <summary>
    /// Charge la configuration de dialogue depuis GeneralConfigManager
    /// </summary>
    private void LoadConfigFromManager()
    {
        if (configLoadedFromManager) return;
        
        Debug.Log($"[DialoguePlayer] LoadConfigFromManager - GeneralConfigManager null? {GeneralConfigManager.Instance == null}");
        
        if (GeneralConfigManager.Instance != null)
        {
            Debug.Log($"[DialoguePlayer] GeneralConfigManager.IsConfigLoaded? {GeneralConfigManager.Instance.IsConfigLoaded()}");
        }
        
        if (GeneralConfigManager.Instance != null && GeneralConfigManager.Instance.IsConfigLoaded())
        {
            var defConfig = GeneralConfigManager.Instance.GetDefaultDialogueConfig();
            Debug.Log($"[DialoguePlayer] defConfig null? {defConfig == null}");
            
            if (defConfig != null)
            {
                Debug.Log($"[DialoguePlayer] 📦 Config JSON: useFrameMode={defConfig.useFrameMode}, frameWidth={defConfig.frameWidth}, frameHeight={defConfig.frameHeight}");
                defaultConfig = ConvertDefaultDialogueToConfig(defConfig);
                configLoadedFromManager = true;
                Debug.Log($"[DialoguePlayer] ✅ Config chargée depuis GeneralConfigManager: useFrameMode={defaultConfig.useFrameMode}, frameWidth={defaultConfig.frameWidth}");
            }
            else
            {
                Debug.LogWarning("[DialoguePlayer] ⚠️ defaultDialogueConfig est null dans GeneralConfigManager!");
            }
        }
        else
        {
            Debug.LogWarning("[DialoguePlayer] ⚠️ Impossible de charger la config - utilisation des valeurs par défaut du code");
            Debug.Log($"[DialoguePlayer] 📦 Config par défaut: useFrameMode={defaultConfig.useFrameMode}, frameWidth={defaultConfig.frameWidth}");
        }
    }
    
    /// <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",
            
            // Position de l'image d'illustration
            illustrationPosition = def.illustrationPosition ?? new IllustrationPositionConfig { x = 1465f, y = 670f },
            illustrationOffsetFromPanel = def.illustrationOffsetFromPanel,

            // Taille / style de l'illustration
            illustrationDiameter = def.illustrationDiameter > 0 ? def.illustrationDiameter : 160f,
            illustrationSize = def.illustrationSize ?? new IllustrationSizeConfig { x = 160f, y = 160f },
            illustrationBorder = def.illustrationBorder ?? new IllustrationBorderConfig { width = 4f, color = "#F4ECE5" },
            illustrationBackgroundColor = !string.IsNullOrEmpty(def.illustrationBackgroundColor) ? def.illustrationBackgroundColor : "#FF0000",
            illustrationBackgroundGradient = def.illustrationBackgroundGradient
        };
    }

    void Update() => HandleInput();

    // ─────────────────────────────────────────────────────────────────────────
    // Media roots / Vidéo UI
    // ─────────────────────────────────────────────────────────────────────────
    public void SetMediaRoots(string imageRoot) => _imageRoot = imageRoot;

    private void EnsureBackgroundUI()
    {
        if (dialoguePanel == null || videoBackgroundUI != null) return;

        var go = new GameObject("DialogueVideoBG");
        go.transform.SetParent(dialoguePanel.transform, false);

        var rt = go.AddComponent<RectTransform>();
        rt.anchorMin = Vector2.zero;
        rt.anchorMax = Vector2.one;
        rt.offsetMin = Vector2.zero;
        rt.offsetMax = Vector2.zero;

        videoBackgroundUI = go.AddComponent<RawImage>();
        videoBackgroundUI.color = Color.white;
        videoBackgroundUI.raycastTarget = false;

        // Derrière tout le reste
        go.transform.SetAsFirstSibling();
        videoBackgroundUI.gameObject.SetActive(false);
    }

    /// <summary>Appelé par le GameManager pour afficher la vidéo de fond (RenderTexture).</summary>
    public void SetDialogueBackgroundTexture(Texture tex)
    {
        EnsureBackgroundUI();
        if (videoBackgroundUI == null) return;

        videoBackgroundUI.texture = tex;
        var c = videoBackgroundUI.color; c.a = 1f; videoBackgroundUI.color = c;
        videoBackgroundUI.gameObject.SetActive(true);
        videoBackgroundUI.transform.SetAsFirstSibling();
    }

public void ClearDialogueBackgroundTexture()
{
    if (videoBackgroundUI == null) return;
    videoBackgroundUI.texture = null;
    // 🔒 Failsafe anti-cadre-blanc
    videoBackgroundUI.gameObject.SetActive(false);
    videoBackgroundUI.enabled = false;
}

    // ─────────────────────────────────────────────────────────────────────────
    // Utils illustration
    // ─────────────────────────────────────────────────────────────────────────
    private string CombineUrl(string root, string leaf)
    {
        if (string.IsNullOrEmpty(leaf)) return null;
        if (string.IsNullOrEmpty(root)) return leaf;
        return root.EndsWith("/") ? (root + leaf) : (root + "/" + leaf);
    }

    private IEnumerator ShowLineIllustration(DialogueLine line)
    {
        int token = _playToken;
        Debug.Log($"[DialoguePlayer] 🖼️ ShowLineIllustration - illustrationImage null? {illustrationImage == null}, container null? {_illustrationContainer == null}");
        
        if (illustrationImage == null)
            yield break;

        if (line == null || string.IsNullOrEmpty(line.image))
        {
            Debug.Log("[DialoguePlayer] 🖼️ Pas d'image pour cette ligne, fade out");
            // pas d'image : fade out
            if (_illustrationCg != null)
            {
                float t = 0f;
                float d = Mathf.Max(0.01f, illustrationFadeDuration);
                float from = _illustrationCg.alpha, to = 0f;
                while (t < d) { t += Time.deltaTime; _illustrationCg.alpha = Mathf.Lerp(from, to, t / d); yield return null; }
                _illustrationCg.alpha = 0f;
            }
            if (_illustrationContainer != null) _illustrationContainer.SetActive(false);
            yield break;
        }

        var url = CombineUrl(_imageRoot, line.image);
        Debug.Log($"[DialoguePlayer] 🖼️ Image demandée: '{line.image}', URL finale: '{url}', imageRoot: '{_imageRoot}'");
        
        if (string.IsNullOrEmpty(url))
        {
            Debug.LogWarning("[DialoguePlayer] ⚠️ URL vide après CombineUrl");
            yield break;
        }

        // même image -> juste fade in si nécessaire
        if (url == _currentImageUrl && illustrationImage.sprite != null)
        {
            Debug.Log("[DialoguePlayer] 🖼️ Même image, fade in uniquement");
            if (_illustrationContainer != null) _illustrationContainer.SetActive(true);
            if (_illustrationCg != null && _illustrationCg.alpha < 1f)
            {
                float t = 0f; float d = Mathf.Max(0.01f, illustrationFadeDuration);
                float from = _illustrationCg.alpha, to = 1f;
                while (t < d) { t += Time.deltaTime; _illustrationCg.alpha = Mathf.Lerp(from, to, t / d); yield return null; }
                _illustrationCg.alpha = 1f;
            }
            yield break;
        }

        Debug.Log($"[DialoguePlayer] 📥 Début téléchargement: {url}");
        using (var req = UnityWebRequestTexture.GetTexture(url))
        {
            yield return req.SendWebRequest();
            if (token != _playToken || !isPlaying) yield break;
#if UNITY_2020_3_OR_NEWER
            if (req.result != UnityWebRequest.Result.Success)
#else
            if (req.isNetworkError || req.isHttpError)
#endif
            {
                Debug.LogWarning("[Dialogue] Load image failed: " + url + " -> " + req.error);
                yield break;
            }

            Debug.Log($"[DialoguePlayer] ✅ Image téléchargée, création du sprite");
            var tex = DownloadHandlerTexture.GetContent(req);
            var sp = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
            
            // Remplacer le sprite (la couleur reste Color.white)
            if (illustrationImage == null) yield break;
            illustrationImage.sprite = sp;
            // PAS BESOIN de changer la couleur, elle est déjà à Color.white
            _currentImageUrl = url;
            
            Debug.Log($"[DialoguePlayer] 🎨 Sprite assigné, container actif? {_illustrationContainer != null && _illustrationContainer.activeSelf}");
            
            // Tracker pour cleanup
            dynamicIllustrationTextures.Add(tex);
            dynamicIllustrationSprites.Add(sp);
            
            // Appliquer position + taille + style depuis la config
            ApplyIllustrationLayout(currentConfig ?? defaultConfig);
            
            // Activer l'image
            illustrationImage.enabled = true;
            if (_illustrationContainer != null) 
            {
                _illustrationContainer.SetActive(true);
                Debug.Log($"[DialoguePlayer] ✅ Container activé, alpha CG avant fade: {_illustrationCg?.alpha}");
            }

            if (_illustrationCg != null)
            {
                float t = 0f; float d = Mathf.Max(0.01f, illustrationFadeDuration);
                float from = _illustrationCg.alpha, to = 1f;
                while (t < d) { t += Time.deltaTime; _illustrationCg.alpha = Mathf.Lerp(from, to, t / d); yield return null; }
                _illustrationCg.alpha = 1f;
                Debug.Log("[DialoguePlayer] ✅ Fade in terminé, alpha = 1");
            }
        }
    }

    private void DestroyCurrentIllustrationResources()
    {
        if (_currentIllustrationSprite != null)
        {
            Destroy(_currentIllustrationSprite);
            _currentIllustrationSprite = null;
        }

        if (_currentIllustrationTexture != null)
        {
            Destroy(_currentIllustrationTexture);
            _currentIllustrationTexture = null;
        }
    }

    private Texture2D TryMakeBackgroundTransparent(Texture2D src)
    {
        if (src == null) return null;
        if (!src.isReadable) return null;

        Color bg = EstimateBgColor(src);
        const float threshold = 0.14f; // tolérance couleur (0..~1) pour supprimer le fond

        Color32[] pixels = src.GetPixels32();
        bool anyChanged = false;
        for (int i = 0; i < pixels.Length; i++)
        {
            Color32 p = pixels[i];
            if (p.a < 10) continue;

            Color c = new Color(p.r / 255f, p.g / 255f, p.b / 255f, p.a / 255f);
            float d = Mathf.Abs(c.r - bg.r) + Mathf.Abs(c.g - bg.g) + Mathf.Abs(c.b - bg.b);
            if (d < threshold)
            {
                pixels[i].a = 0;
                anyChanged = true;
            }
        }

        if (!anyChanged) return null;

        Texture2D dst = new Texture2D(src.width, src.height, TextureFormat.RGBA32, false);
        dst.filterMode = src.filterMode;
        dst.wrapMode = TextureWrapMode.Clamp;
        dst.SetPixels32(pixels);
        dst.Apply(false, false);
        return dst;
    }

    private Color EstimateBgColor(Texture2D tex)
    {
        // On échantillonne plusieurs points au bord : si l'avatar a un rond gris, ces points tombent dessus.
        int w = tex.width;
        int h = tex.height;
        Vector2Int[] samples = new Vector2Int[]
        {
            new Vector2Int(Mathf.RoundToInt(w * 0.12f), Mathf.RoundToInt(h * 0.50f)),
            new Vector2Int(Mathf.RoundToInt(w * 0.88f), Mathf.RoundToInt(h * 0.50f)),
            new Vector2Int(Mathf.RoundToInt(w * 0.50f), Mathf.RoundToInt(h * 0.12f)),
            new Vector2Int(Mathf.RoundToInt(w * 0.50f), Mathf.RoundToInt(h * 0.88f)),
        };

        Color acc = Color.clear;
        int count = 0;
        for (int i = 0; i < samples.Length; i++)
        {
            Color c = tex.GetPixel(samples[i].x, samples[i].y);
            if (c.a > 0.6f)
            {
                acc += c;
                count++;
            }
        }

        if (count == 0)
        {
            // Fallback: gris clair
            return new Color(0.86f, 0.86f, 0.86f, 1f);
        }

        acc /= count;
        acc.a = 1f;
        return acc;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // API publique
    // ─────────────────────────────────────────────────────────────────────────
public void PlayDialogue(DialogueSequence dialogue, DialogueConfig config = null)
{
    if (dialogue == null) { Debug.LogError("DialoguePlayer: Dialogue sequence is null!"); return; }
    if (dialogue.lines == null || dialogue.lines.Count == 0) { Debug.LogError("DialoguePlayer: Dialogue has no lines!"); return; }

    // Invalider toute coroutine précédente (images/vidéos/downloads) avant de démarrer
    _playToken++;
    if (isPlaying) StopDialogue();

    currentDialogue = dialogue;
    currentConfig   = config ?? defaultConfig;
    currentLineIndex = 0;
    isPlaying = true;

    // —> visible au-dessus TOUT DE SUITE (au cas où la fade serait freinée par timescale)
    ForceShowPanel();

    Debug.Log($"[DialoguePlayer] Starting dialogue '{dialogue.title}' with {dialogue.lines.Count} lines");
    Debug.Log($"[DialoguePlayer] Config: requireInputToAdvance={currentConfig.requireInputToAdvance}, nextKey={currentConfig.nextKey}, prevKey={currentConfig.prevKey}, skip={currentConfig.skipDialogueKey}");

    SetupDialogueUI();
    dialogueCoroutine = StartCoroutine(PlayDialogueSequence());
}


public void StopDialogue()
{
    // Invalider toute coroutine de chargement en vol
    _playToken++;

    if (dialogueCoroutine != null)
    {
        StopCoroutine(dialogueCoroutine);
        dialogueCoroutine = null;
    }
    if (_overlayLoadCo != null)
    {
        StopCoroutine(_overlayLoadCo);
        _overlayLoadCo = null;
    }
    if (_playFromUrlCoroutine != null)
    {
        StopCoroutine(_playFromUrlCoroutine);
        _playFromUrlCoroutine = null;
    }
    if (_continueIndicatorLoadCoroutine != null)
    {
        StopCoroutine(_continueIndicatorLoadCoroutine);
        _continueIndicatorLoadCoroutine = null;
    }

    isPlaying = false;
    isWaitingForInput = false;

    // 🔧 CORRECTION : Faire un fade out propre au lieu de couper brutalement
    // Best-effort: si on est en teardown (changement de scène), on préfère un cleanup dur.
    try { StartCoroutine(FadeOutDialogue()); }
    catch { ForceCleanupAndHide(); }

    Debug.Log("DialoguePlayer: Dialogue stopped");
}

    public bool IsPlayingDialogue() => isPlaying;

    public void NextLine()
    {
        if (!isPlaying || !isWaitingForInput) return;
        isWaitingForInput = false;
        
        // 🔧 CORRECTION : Si on est déjà à la dernière ligne, on ferme le dialogue au lieu d'incrémenter
        if (currentLineIndex >= currentDialogue.lines.Count - 1)
        {
            Debug.Log("[DialoguePlayer] 🚪 Dernière ligne atteinte - fermeture du dialogue");
            StopDialogue();
        }
        else
        {
            currentLineIndex++;
        }
    }

    public void PrevLine()
    {
        if (!isPlaying || !isWaitingForInput) return;
        isWaitingForInput = false;
        currentLineIndex = Mathf.Max(0, currentLineIndex - 1);
    }

    public void SkipDialogue()
    {
        if (!isPlaying) return;
        Debug.Log("DialoguePlayer: Dialogue skipped by user");
        StopDialogue();
        OnDialogueSkipped?.Invoke();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Coroutine principale (lecture)
    // ─────────────────────────────────────────────────────────────────────────
    IEnumerator PlayDialogueSequence()
    {
        // Apparition
        yield return StartCoroutine(FadeInDialogue());

        if (currentDialogue == null || currentDialogue.lines == null || currentDialogue.lines.Count == 0)
        {
            Debug.LogError("[DialoguePlayer] ❌ Dialogue invalide ou vide !");
            isPlaying = false;
            OnDialogueComplete?.Invoke();
            yield break;
        }
        
        Debug.Log($"[DialoguePlayer] ✅ Dialogue chargé avec {currentDialogue.lines.Count} lignes");

        // Boucle de lecture
        while (currentLineIndex >= 0 && currentLineIndex < currentDialogue.lines.Count)
        {
            Debug.Log($"[DialoguePlayer] 📖 Ligne {currentLineIndex + 1}/{currentDialogue.lines.Count}");
            DialogueLine line = currentDialogue.lines[currentLineIndex];

            // Délai avant affichage
            if (line != null && line.delay > 0f)
                yield return new WaitForSeconds(line.delay);

            // Speaker par défaut si vide
            if (line != null && string.IsNullOrEmpty(line.speaker) && !string.IsNullOrEmpty(currentDialogue.speaker))
                line.speaker = currentDialogue.speaker;

            // Illustration (si dispo)
            if (line != null)
                yield return StartCoroutine(ShowLineIllustration(line));

            // Afficher
            DisplayLine(line);
            OnLineDisplayed?.Invoke(line);

            // Attente d’avancement
            isWaitingForInput = true;

            // ► Mode navigation 100% manuelle : on ignore toute durée
            if (currentConfig != null && currentConfig.requireInputToAdvance)
            {
                yield return new WaitUntil(() => !isWaitingForInput || !isPlaying);
            }
            else
            {
                // Auto-advance si durée ligne > 0 sinon durée config > 0, sinon input
                float autoAdvance = 0f;
                if (line != null)
                {
                    var f = line.GetType().GetField("duration");
                    if (f != null)
                    {
                        try { autoAdvance = (float)f.GetValue(line); } catch { }
                    }
                }
                if (autoAdvance <= 0f && currentConfig != null && currentConfig.autoAdvanceDelay > 0f)
                    autoAdvance = currentConfig.autoAdvanceDelay;

                if (autoAdvance > 0f)
                {
                    float elapsed = 0f;
                    while (elapsed < autoAdvance && isWaitingForInput && isPlaying)
                    {
                        elapsed += Time.deltaTime;
                        yield return null;
                    }
                    if (isWaitingForInput && isPlaying)
                        NextLine();
                }
                else
                {
                    yield return new WaitUntil(() => !isWaitingForInput || !isPlaying);
                }
            }

            if (!isPlaying) yield break; // Skip/Stop
        }

        // Fin
        Debug.Log($"[DialoguePlayer] 🏁 Fin du dialogue atteinte - currentLineIndex={currentLineIndex}, total lines={currentDialogue.lines.Count}");
        
        // 🔧 CORRECTION : Ne pas faire de fade out automatique pour les dialogues post-jeu
        // Le dialogue doit rester visible jusqu'à ce qu'un événement externe le ferme
        // yield return StartCoroutine(FadeOutDialogue());
        
        isPlaying = false;
        Debug.Log($"DialoguePlayer: Dialogue '{currentDialogue.title}' completed");
        OnDialogueComplete?.Invoke();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Affichage UI
    // ─────────────────────────────────────────────────────────────────────────
    void DisplayLine(DialogueLine line)
    {
        Debug.Log($"[DialoguePlayer] 📝 DisplayLine appelé - line null? {line == null}, line.image: '{line?.image ?? "null"}'");
        
        DisableEmptyUIRenderers();
        
        // 🔧 CORRECTION : S'assurer que le panneau et ses éléments sont activés avant d'afficher la ligne
        if (dialoguePanel != null)
        {
            dialoguePanel.SetActive(true);
        }
        if (bottomBarRect != null)
        {
            bottomBarRect.gameObject.SetActive(true);
        }
        if (bottomBarBg != null)
        {
            bottomBarBg.gameObject.SetActive(true);
            bottomBarBg.enabled = true;
        }
        
        if (dialogueText != null)
        {
            // 🔧 CORRECTION : S'assurer que le texte est activé et visible
            dialogueText.gameObject.SetActive(true);
            dialogueText.enabled = true;
            string textToDisplay = line?.text ?? "";
            // Remplacer les séquences échappées \n par de vrais retours à la ligne
            // Gérer plusieurs formats possibles : \\n (échappé JSON), \n (littéral), et aussi les vraies nouvelles lignes
            textToDisplay = textToDisplay.Replace("\\n", "\n");  // JSON échappé
            textToDisplay = textToDisplay.Replace("\\r\\n", "\n");  // Windows line endings
            textToDisplay = textToDisplay.Replace("\\r", "\n");  // Mac line endings
            // S'assurer que TextMeshPro interprète les retours à la ligne
            dialogueText.text = textToDisplay;
            dialogueText.textWrappingMode = TMPro.TextWrappingModes.Normal;  // Activer le retour à la ligne automatique
        }

        if (speakerText != null)
        {
            // 🔧 CORRECTION : S'assurer que le texte est activé et visible
            string who = (!string.IsNullOrEmpty(line?.speaker)) ? line.speaker : "";
            speakerText.text = who;
            speakerText.gameObject.SetActive(true);
            speakerText.enabled = true;
        }

        // --- Gestion des images ---
        // Si tu utilises une Image dédiée "illustrationImage", laisse l'overlay OFF.
        Debug.Log($"[DialoguePlayer] DisplayLine - illustrationImage null? {illustrationImage == null}, overlayImage null? {overlayImage == null}, line.image: '{line?.image ?? "null"}'");
        
        if (illustrationImage != null)
        {
            // Utiliser le système illustrationImage (avec cercle rouge)
            StartCoroutine(ShowLineIllustration(line));
            
            // Désactiver overlayImage pour éviter la duplication
            if (overlayImage != null)
            {
                overlayImage.sprite = null;
                overlayImage.enabled = false;
                overlayImage.gameObject.SetActive(false);
            }
        }
        else if (overlayImage != null)
        {
            // Stoppe un chargement précédent (cas: navigation rapide)
            if (_overlayLoadCo != null)
            {
                StopCoroutine(_overlayLoadCo);
                _overlayLoadCo = null;
            }

            if (!string.IsNullOrEmpty(line?.image))
            {
                // charge l'image et n'allume l'overlay qu'en cas de succès
                _overlayLoadCo = StartCoroutine(LoadOverlayImage(line.image));
            }
            else
            {
                // pas d'image pour cette ligne -> coupe tout
                overlayImage.sprite = null;
                overlayImage.enabled = false;
                overlayImage.gameObject.SetActive(false);
            }
        }

        string preview = line?.text ?? "";
        if (preview.Length > 50) preview = preview.Substring(0, 50) + "...";
        Debug.Log($"DialoguePlayer: Displaying line - Speaker: '{line?.speaker ?? ""}', Text: '{preview}'");
    }

    IEnumerator LoadOverlayImage(string imageUrl)
    {
        int token = _playToken;
        string finalUrl = CombineUrl(_imageRoot, imageUrl);

        using (var www = UnityWebRequestTexture.GetTexture(finalUrl))
        {
            yield return www.SendWebRequest();
            if (token != _playToken || !isPlaying) { _overlayLoadCo = null; yield break; }

    #if UNITY_2020_3_OR_NEWER
            if (www.result == UnityWebRequest.Result.Success)
    #else
            if (!www.isNetworkError && !www.isHttpError)
    #endif
            {
                Texture2D tex = ((DownloadHandlerTexture)www.downloadHandler).texture;
                Sprite sprite = Sprite.Create(tex, new Rect(0,0,tex.width,tex.height), new Vector2(0.5f,0.5f));
                
                Debug.Log($"[DialoguePlayer] 🖼️ Image chargée: {tex.width}x{tex.height}");
                
                if (overlayImage == null) 
                { 
                    Debug.LogError("[DialoguePlayer] ❌ overlayImage est NULL!");
                    _overlayLoadCo = null; 
                    yield break; 
                }
                
                overlayImage.sprite = sprite;
                overlayImage.preserveAspect = true;
                overlayImage.enabled = true;
                overlayImage.gameObject.SetActive(true);
                
                Debug.Log($"[DialoguePlayer] 🖼️ overlayImage.gameObject.activeSelf = {overlayImage.gameObject.activeSelf}");
                Debug.Log($"[DialoguePlayer] 🖼️ overlayImage.enabled = {overlayImage.enabled}");
                
                // Positionner l'image selon la configuration du JSON
                RectTransform imgRect = overlayImage.rectTransform;
                
                // Toujours utiliser le système absolu pour overlayImage car elle n'est pas enfant du panneau
                float posX = currentConfig?.illustrationPosition?.x ?? 1465f;
                float posY = currentConfig?.illustrationPosition?.y ?? 670f;
                float anchoredX = posX - 960f;
                float anchoredY = posY - 540f;
                
                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($"[DialoguePlayer] 🖼️ OverlayImage positionnée à ({posX}, {posY}) -> anchoredPosition ({anchoredX}, {anchoredY})");
                Debug.Log($"[DialoguePlayer] 🖼️ Parent: {overlayImage.transform.parent?.name}, SizeDelta: {imgRect.sizeDelta}");
            }
            else
            {
                Debug.LogError($"DialoguePlayer: Failed to load overlay image: {www.error} ({finalUrl})");
                if (overlayImage != null)
                {
                    overlayImage.sprite = null;
                    overlayImage.enabled = false;
                    overlayImage.gameObject.SetActive(false);
                }
            }
        }

        _overlayLoadCo = null;
    }

    void SetupDialogueUI()
    {
        if (dialoguePanel == null) return;

        // 🔧 S'assure d'avoir les refs même si l'UI vient d'un prefab
        EnsureBottomBarRefs();
        
        Debug.Log($"[DialoguePlayer] SetupDialogueUI - bottomBarBg null? {bottomBarBg == null}, currentConfig null? {currentConfig == null}");
        if (currentConfig != null)
        {
            Debug.Log($"[DialoguePlayer] SetupDialogueUI - Config: useFrameMode={currentConfig.useFrameMode}, frameWidth={currentConfig.frameWidth}, frameHeight={currentConfig.frameHeight}");
            Debug.Log($"[DialoguePlayer] SetupDialogueUI - bottomBarColor={currentConfig.bottomBarColor}, backgroundDimming={currentConfig.backgroundDimming}");
        }

        // — textes
        if (dialogueText != null)
        {
            // 🔧 CORRECTION : S'assurer que le texte est activé et visible
            dialogueText.gameObject.SetActive(true);
            dialogueText.enabled = true;
            dialogueText.fontSize = currentConfig.dialogueTextSize;
            if (ColorUtility.TryParseHtmlString(currentConfig.dialogueTextColor, out Color textColor))
            {
                textColor.a = 1f; // S'assurer que l'alpha est à 1 pour la visibilité
                dialogueText.color = textColor;
            }
        }
        if (speakerText != null)
        {
            // 🔧 CORRECTION : S'assurer que le texte est activé et visible
            speakerText.gameObject.SetActive(true);
            speakerText.enabled = true;
            speakerText.fontSize = currentConfig.speakerTextSize;
            if (ColorUtility.TryParseHtmlString(currentConfig.speakerTextColor, out Color speakerColor))
            {
                speakerColor.a = 1f; // S'assurer que l'alpha est à 1 pour la visibilité
                speakerText.color = speakerColor;
            }
        }

        // — instructions / indicateur de continuation
        if (currentConfig.useFrameMode)
        {
            Debug.Log("[DialoguePlayer] 🎨 SetupDialogueUI: MODE FRAME détecté");
            
            // Mode frame : utiliser l'image PNG
            if (continueIndicatorImage != null)
            {
                continueIndicatorImage.gameObject.SetActive(currentConfig.showContinueIndicator);
                Debug.Log($"[DialoguePlayer] ✅ Image PNG activée: {currentConfig.showContinueIndicator}");
            }
            else
            {
                Debug.LogWarning("[DialoguePlayer] ⚠️ continueIndicatorImage est null en mode frame!");
            }
            
            // Désactiver le texte legacy
            if (skipInstructionText != null)
            {
                skipInstructionText.gameObject.SetActive(false);
                Debug.Log("[DialoguePlayer] ❌ TextMeshProUGUI désactivé (mode frame)");
            }
        }
        else
        {
            Debug.Log("[DialoguePlayer] 📝 SetupDialogueUI: MODE LEGACY détecté");
            
            // Mode legacy : utiliser le texte
            if (skipInstructionText != null)
            {
                skipInstructionText.gameObject.SetActive(currentConfig.showSkipInstructions);
                if (currentConfig.showSkipInstructions)
                {
                    // Utiliser le texte de la config s'il existe, sinon générer dynamiquement
                    if (!string.IsNullOrEmpty(currentConfig.instructionsText))
                    {
                        skipInstructionText.text = currentConfig.instructionsText;
                    }
                    else
                    {
                        skipInstructionText.text =
                            $"←/{currentConfig.prevKey} : Reculer   •   {currentConfig.nextKey} : Suivant   •   {currentConfig.skipDialogueKey} : Skip";
                    }
                }
                Debug.Log($"[DialoguePlayer] ✅ TextMeshProUGUI activé: {currentConfig.showSkipInstructions}");
            }
            
            // Désactiver l'image PNG
            if (continueIndicatorImage != null)
            {
                continueIndicatorImage.gameObject.SetActive(false);
                Debug.Log("[DialoguePlayer] ❌ Image PNG désactivée (mode legacy)");
            }
        }

        // — cadre de dialogue (mode frame ou bandeau legacy)
        if (currentConfig.useFrameMode)
        {
            // Mode frame : cadre centré avec dimensions fixes
            if (bottomBarRect != null)
            {
                // 🔧 CORRECTION : S'assurer que le panneau est activé et visible
                bottomBarRect.gameObject.SetActive(true);
                bottomBarRect.anchorMin = new Vector2(0.5f, 0f);
                bottomBarRect.anchorMax = new Vector2(0.5f, 0f);
                bottomBarRect.pivot = new Vector2(0.5f, 0f);
                bottomBarRect.sizeDelta = new Vector2(currentConfig.frameWidth, currentConfig.frameHeight);
                bottomBarRect.anchoredPosition = new Vector2(0, currentConfig.frameBottomMargin);
                Debug.Log($"[DialoguePlayer] ✅ Mode FRAME: {currentConfig.frameWidth}x{currentConfig.frameHeight} à {currentConfig.frameBottomMargin}px du bas");
            }
            
            if (bottomBarBg != null)
            {
                // 🔧 CORRECTION : S'assurer que l'Image est activée et visible
                bottomBarBg.gameObject.SetActive(true);
                bottomBarBg.enabled = true;
                if (ColorUtility.TryParseHtmlString(currentConfig.frameBackgroundColor, out var frameCol))
                {
                    frameCol.a = 1f; // S'assurer que l'alpha est à 1 pour la visibilité
                    bottomBarBg.color = frameCol;
                    Debug.Log($"[DialoguePlayer] ✅ Fond cadre: {currentConfig.frameBackgroundColor}");
                }
                else
                {
                    // Fallback : couleur par défaut avec alpha à 1
                    bottomBarBg.color = new Color(0.96f, 0.93f, 0.90f, 1f);
                }
            }
            
            // Appliquer/mettre à jour les coins arrondis
            if (currentConfig.frameRadius > 0)
            {
                if (bottomBarRoundedCorners == null && bottomBarRect != null)
                {
                    bottomBarRoundedCorners = bottomBarRect.gameObject.AddComponent<RoundedCornersImage>();
                }
                if (bottomBarRoundedCorners != null)
                {
                    bottomBarRoundedCorners.cornerRadius = currentConfig.frameRadius;
                }
            }
            
            // Positionner les éléments internes en mode frame
            float padL = currentConfig.framePaddingLeft;
            float padR = currentConfig.framePaddingRight;
            float padT = currentConfig.framePaddingTop;
            float padB = currentConfig.framePaddingBottom;
            
            // Padding intérieur supplémentaire pour le conteneur titre+sous-titre
            float tcPadL = currentConfig.textContentPaddingLeft;
            float tcPadR = currentConfig.textContentPaddingRight;
            float tcPadT = currentConfig.textContentPaddingTop;
            float tcPadB = currentConfig.textContentPaddingBottom;
            
            // Padding total = frame padding + text content padding
            float totalPadL = padL + tcPadL;
            float totalPadR = padR + tcPadR;
            float totalPadT = padT + tcPadT;
            float totalPadB = padB + tcPadB;
            
            if (speakerRectRt != null)
            {
                speakerRectRt.anchorMin = new Vector2(0, 1f);
                speakerRectRt.anchorMax = new Vector2(1, 1f);
                speakerRectRt.pivot = new Vector2(0.5f, 1f);
                speakerRectRt.anchoredPosition = new Vector2(0, -totalPadT);
                speakerRectRt.sizeDelta = new Vector2(-totalPadL - totalPadR, 50);
                speakerRectRt.offsetMin = new Vector2(totalPadL, speakerRectRt.offsetMin.y);
                speakerRectRt.offsetMax = new Vector2(-totalPadR, speakerRectRt.offsetMax.y);
                
                if (speakerText != null)
                {
                    speakerText.fontStyle = currentConfig.speakerTextBold ? FontStyles.Bold : FontStyles.Normal;
                }
            }
            
            if (dialogueRectRt != null)
            {
                dialogueRectRt.anchorMin = new Vector2(0, 0f);
                dialogueRectRt.anchorMax = new Vector2(1, 1f);
                dialogueRectRt.offsetMin = new Vector2(totalPadL, totalPadB + 40);
                dialogueRectRt.offsetMax = new Vector2(-totalPadR, -totalPadT - 50 - currentConfig.speakerMarginBottom);
                
                if (dialogueText != null)
                {
                    // Alignement du texte selon la config
                    switch (currentConfig.dialogueTextAlignment.ToLower())
                    {
                        case "center":
                            dialogueText.alignment = TextAlignmentOptions.TopJustified;
                            break;
                        case "right":
                            dialogueText.alignment = TextAlignmentOptions.TopRight;
                            break;
                        default:
                            dialogueText.alignment = TextAlignmentOptions.TopLeft;
                            break;
                    }
                }
            }
            
            if (instructionsRectRt != null)
            {
                instructionsRectRt.anchorMin = new Vector2(0.5f, 0f);
                instructionsRectRt.anchorMax = new Vector2(0.5f, 0f);
                instructionsRectRt.pivot = new Vector2(0.5f, 0f);
                instructionsRectRt.anchoredPosition = new Vector2(0, currentConfig.continueIndicatorBottomMargin);
                instructionsRectRt.sizeDelta = new Vector2(100, 30);
            }
            
            // Mode frame : afficher l'image PNG, masquer le texte
            if (continueIndicatorImage != null)
            {
                continueIndicatorImage.gameObject.SetActive(currentConfig.showContinueIndicator);
            }
            if (skipInstructionText != null)
            {
                skipInstructionText.gameObject.SetActive(false);
            }
        }
        else
        {
            // Mode legacy : bandeau pleine largeur
            if (bottomBarRect != null)
            {
                // 🔧 CORRECTION : S'assurer que le panneau est activé et visible
                bottomBarRect.gameObject.SetActive(true);
                float ratio = Mathf.Clamp01(currentConfig.bottomBarHeightRatio);
                bottomBarRect.anchorMin = new Vector2(0f, 0f);
                bottomBarRect.anchorMax = new Vector2(1f, ratio);
                bottomBarRect.offsetMin = Vector2.zero;
                bottomBarRect.offsetMax = Vector2.zero;
            }
            if (bottomBarBg != null)
            {
                // 🔧 CORRECTION : S'assurer que l'Image est activée et visible
                bottomBarBg.gameObject.SetActive(true);
                bottomBarBg.enabled = true;
                if (ColorUtility.TryParseHtmlString(currentConfig.bottomBarColor, out var barCol))
                {
                    barCol.a = Mathf.Clamp01(currentConfig.backgroundDimming);
                    bottomBarBg.color = barCol;
                    Debug.Log($"[DialoguePlayer] ✅ Bandeau coloré appliqué: {currentConfig.bottomBarColor} avec alpha={barCol.a}");
                }
                else
                {
                    // Fallback : couleur par défaut
                    bottomBarBg.color = new Color(0, 0, 0, Mathf.Clamp01(currentConfig.backgroundDimming));
                }
            }

            float padL = currentConfig.paddingLeft;
            float padR = currentConfig.paddingRight;

            if (speakerRectRt != null)
            {
                speakerRectRt.offsetMin = new Vector2(padL, speakerRectRt.offsetMin.y);
                speakerRectRt.offsetMax = new Vector2(-padR, speakerRectRt.offsetMax.y);
            }
            if (dialogueRectRt != null)
            {
                dialogueRectRt.offsetMin = new Vector2(padL, dialogueRectRt.offsetMin.y);
                dialogueRectRt.offsetMax = new Vector2(-padR, dialogueRectRt.offsetMax.y);
            }
            if (instructionsRectRt != null)
            {
                instructionsRectRt.offsetMin = new Vector2(padL, instructionsRectRt.offsetMin.y);
                instructionsRectRt.offsetMax = new Vector2(-padR, instructionsRectRt.offsetMax.y);
            }
        }

        // — voile : 1.0 tant que pas de vidéo pour ne PAS voir le jeu ; dimming quand la vidéo est là
        if (backgroundImage != null)
        {
            bool hasVideo = (videoBackgroundUI != null && videoBackgroundUI.texture != null && videoBackgroundUI.gameObject.activeInHierarchy);
            float targetA = hasVideo ? Mathf.Clamp01(currentConfig.backgroundDimming) : 1f;
            var bg = backgroundImage.color; bg.a = targetA; backgroundImage.color = bg;
        }

        Debug.Log("DialoguePlayer: UI configured (bottom bar + colors + padding).");
        Debug.Log($"[DialoguePlayer] Apply bottom bar: height={currentConfig.bottomBarHeightRatio}, color={currentConfig.bottomBarColor}, alpha={currentConfig.backgroundDimming}, padL={currentConfig.paddingLeft}, padR={currentConfig.paddingRight}");
    }


    // méthode utilitaire à ajouter dans la classe
    public void ForceHideAllUI()
    {
        if (overlayImage != null)
        {
            overlayImage.sprite = null;
            overlayImage.enabled = false;
            overlayImage.gameObject.SetActive(false);
        }
        if (illustrationImage != null)
        {
            illustrationImage.enabled = false;
            if (_illustrationCg != null) _illustrationCg.alpha = 0f;
        }
        if (dialoguePanel != null)
            dialoguePanel.SetActive(false);

        // si tu utilises un RawImage vidéo
        if (videoBackgroundUI != null)
        {
            videoBackgroundUI.texture = null;
            videoBackgroundUI.gameObject.SetActive(false);
        }
    }



IEnumerator FadeInDialogue()
{
    if (dialoguePanel == null) yield break;

    dialoguePanel.SetActive(true);
    var cg = dialoguePanel.GetComponent<CanvasGroup>();
    if (cg == null) cg = dialoguePanel.AddComponent<CanvasGroup>();
    cg.alpha = 0f;

    // 🔧 CORRECTION : S'assurer que tous les éléments du panneau sont activés avant le fade-in
    if (bottomBarRect != null)
    {
        bottomBarRect.gameObject.SetActive(true);
    }
    if (bottomBarBg != null)
    {
        bottomBarBg.gameObject.SetActive(true);
        bottomBarBg.enabled = true;
    }
    if (dialogueText != null)
    {
        dialogueText.gameObject.SetActive(true);
        dialogueText.enabled = true;
    }
    if (speakerText != null)
    {
        speakerText.gameObject.SetActive(true);
        speakerText.enabled = true;
    }

    float dur = (currentConfig != null) ? currentConfig.fadeTransitionDuration : 0.35f;
    float t = 0f;
    while (t < dur)
    {
        t += Time.unscaledDeltaTime;
        cg.alpha = Mathf.Lerp(0f, 1f, Mathf.Clamp01(t / dur));
        yield return null;
    }
    cg.alpha = 1f;
}

IEnumerator FadeOutDialogue()
{
    if (dialoguePanel == null) yield break;

    var cg = dialoguePanel.GetComponent<CanvasGroup>();
    if (cg == null) cg = dialoguePanel.AddComponent<CanvasGroup>();
    cg.alpha = 1f;

    float dur = (currentConfig != null) ? currentConfig.fadeTransitionDuration : 0.35f;
    float t = 0f;
    while (t < dur)
    {
        t += Time.unscaledDeltaTime;
        cg.alpha = Mathf.Lerp(1f, 0f, Mathf.Clamp01(t / dur));
        yield return null;
    }
    cg.alpha = 0f;

    // éteint l'UI du dialogue
    dialoguePanel.SetActive(false);

    // 🔧 coupe la vidéo et son RawImage pour éviter le "cadre blanc"
    StopBackgroundVideo();

    // nettoie les images d'overlay/illustration
    if (overlayImage != null)
    {
        overlayImage.sprite = null;
        overlayImage.enabled = false;
        overlayImage.gameObject.SetActive(false);
    }
    if (illustrationImage != null)
    {
        illustrationImage.enabled = false;
        if (_illustrationCg != null) _illustrationCg.alpha = 0f;
    }

    HardCleanupVisuals();
}


    void CreateDialogueUI()
    {
        // Trouver le Canvas principal, PAS le PopupContainer
        Canvas canvas = null;
        #if UNITY_2023_1_OR_NEWER
            Canvas[] allCanvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None);
        #else
            Canvas[] allCanvases = FindObjectsOfType<Canvas>();
        #endif
        
        foreach (Canvas c in allCanvases)
        {
            // Prendre le Canvas nommé "Canvas" (le principal)
            if (c.gameObject.name == "Canvas")
            {
                canvas = c;
                break;
            }
        }
        
        // Si pas trouvé, prendre le premier qui n'est pas PopupContainer
        if (canvas == null)
        {
            foreach (Canvas c in allCanvases)
            {
                if (c.gameObject.name != "PopupContainer")
                {
                    canvas = c;
                    break;
                }
            }
        }

        if (canvas == null)
        {
            Debug.LogError("DialoguePlayer: No Canvas found for auto-creation");
            return;
        }
        
        Debug.Log($"[DialoguePlayer] Canvas trouvé pour UI: {canvas.gameObject.name}");

        // Panel plein écran
        GameObject panel = new GameObject("DialoguePanel");
        panel.transform.SetParent(canvas.transform, false);

        RectTransform panelRect = panel.AddComponent<RectTransform>();
        panelRect.anchorMin = Vector2.zero;
        panelRect.anchorMax = Vector2.one;
        panelRect.offsetMin = Vector2.zero;
        panelRect.offsetMax = Vector2.zero;

        CanvasGroup canvasGroup = panel.AddComponent<CanvasGroup>();

        // Fond (alpha appliqué dans SetupDialogueUI)
        Image bgImage = panel.AddComponent<Image>();
        bgImage.color = new Color(0, 0, 0, 0f);
        bgImage.raycastTarget = true; // Permettre les clics pour passer les dialogues
        
        // CORRECTION MAC : Sur Mac, forcer l'invisibilité complète
        #if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
        bgImage.enabled = false;
        Debug.Log("[DialoguePlayer] MAC DETECTED - Fond du dialogue désactivé pour éviter le voile blanc");
        #endif
        
        backgroundImage = bgImage;

       
        // Canvas overlay
        Canvas dialogueCanvas = panel.AddComponent<Canvas>();
        dialogueCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        dialogueCanvas.overrideSorting = true;
        dialogueCanvas.sortingOrder = 50000; // très haut
        
        // AJOUT : CanvasScaler pour forcer 1920x1080
        CanvasScaler scaler = panel.AddComponent<CanvasScaler>();
        scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        scaler.referenceResolution = new Vector2(1920, 1080);
        scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
        scaler.matchWidthOrHeight = 0.5f; // Équilibre largeur/hauteur
        Debug.Log("[DialoguePlayer] ✅ CanvasScaler ajouté pour résolution 1920x1080");

        // ===== Cadre de dialogue (mode frame ou bandeau legacy) =====
        GameObject textArea = new GameObject("BottomBar");
        textArea.transform.SetParent(panel.transform, false);

        bottomBarRect = textArea.AddComponent<RectTransform>();
        
        // Mode frame par défaut : cadre centré avec dimensions fixes
        if (defaultConfig.useFrameMode)
        {
            // Ancre au centre-bas
            bottomBarRect.anchorMin = new Vector2(0.5f, 0f);
            bottomBarRect.anchorMax = new Vector2(0.5f, 0f);
            bottomBarRect.pivot = new Vector2(0.5f, 0f);
            bottomBarRect.sizeDelta = new Vector2(defaultConfig.frameWidth, defaultConfig.frameHeight);
            bottomBarRect.anchoredPosition = new Vector2(0, defaultConfig.frameBottomMargin);
            Debug.Log($"[DialoguePlayer] ✅ Mode FRAME activé: {defaultConfig.frameWidth}x{defaultConfig.frameHeight}, margin={defaultConfig.frameBottomMargin}");
        }
        else
        {
            // Mode legacy : bandeau pleine largeur
            bottomBarRect.anchorMin = new Vector2(0f, 0f);
            bottomBarRect.anchorMax = new Vector2(1f, 0.32f);
            bottomBarRect.offsetMin = Vector2.zero;
            bottomBarRect.offsetMax = Vector2.zero;
            Debug.Log("[DialoguePlayer] ✅ Mode BANDEAU legacy activé");
        }

        bottomBarBg = textArea.AddComponent<Image>();
        // Couleur par défaut selon le mode
        if (defaultConfig.useFrameMode)
        {
            if (ColorUtility.TryParseHtmlString(defaultConfig.frameBackgroundColor, out Color frameCol))
                bottomBarBg.color = frameCol;
            else
                bottomBarBg.color = new Color(0.96f, 0.93f, 0.90f, 1f); // #f5ece5
            
            // Ajouter les coins arrondis si le radius est > 0
            if (defaultConfig.frameRadius > 0)
            {
                bottomBarRoundedCorners = textArea.AddComponent<RoundedCornersImage>();
                bottomBarRoundedCorners.cornerRadius = defaultConfig.frameRadius;
                Debug.Log($"[DialoguePlayer] ✅ Coins arrondis appliqués: radius={defaultConfig.frameRadius}");
            }
        }
        else
        {
            bottomBarBg.color = new Color(0, 0, 0, 0.6f);
        }
        bottomBarBg.enabled = true;
        
        // Appliquer le raycast block pour les clics
        bottomBarBg.raycastTarget = true;
        
        textArea.SetActive(true);
        Debug.Log("[DialoguePlayer] ✅ Cadre dialogue créé et activé");

        // Speaker (titre en violet)
        GameObject speakerObj = new GameObject("SpeakerText");
        speakerObj.transform.SetParent(textArea.transform, false);

        speakerRectRt = speakerObj.AddComponent<RectTransform>();
        if (defaultConfig.useFrameMode)
        {
            // Mode frame : positionnement absolu depuis le haut
            // Calcul du padding total (frame + textContent)
            float initTotalPadL = defaultConfig.framePaddingLeft + defaultConfig.textContentPaddingLeft;
            float initTotalPadR = defaultConfig.framePaddingRight + defaultConfig.textContentPaddingRight;
            float initTotalPadT = defaultConfig.framePaddingTop + defaultConfig.textContentPaddingTop;
            
            speakerRectRt.anchorMin = new Vector2(0, 1f);
            speakerRectRt.anchorMax = new Vector2(1, 1f);
            speakerRectRt.pivot = new Vector2(0.5f, 1f);
            speakerRectRt.anchoredPosition = new Vector2(0, -initTotalPadT);
            speakerRectRt.sizeDelta = new Vector2(-initTotalPadL - initTotalPadR, 50);
        }
        else
        {
            speakerRectRt.anchorMin = new Vector2(0, 0.66f);
            speakerRectRt.anchorMax = new Vector2(1, 0.98f);
            speakerRectRt.offsetMin = new Vector2(24, 0);
            speakerRectRt.offsetMax = new Vector2(-24, 0);
        }

        speakerText = speakerObj.AddComponent<TextMeshProUGUI>();
        speakerText.text = "";
        speakerText.fontSize = defaultConfig.speakerTextSize;
        if (ColorUtility.TryParseHtmlString(defaultConfig.speakerTextColor, out Color spkCol))
            speakerText.color = spkCol;
        else
            speakerText.color = new Color(0.39f, 0.28f, 0.50f, 1f); // #64477f violet
        speakerText.fontStyle = defaultConfig.speakerTextBold ? FontStyles.Bold : FontStyles.Normal;
        speakerText.alignment = TextAlignmentOptions.TopLeft;

        // Texte principal (sous-titre aligné à gauche)
        GameObject dialogueObj = new GameObject("DialogueText");
        dialogueObj.transform.SetParent(textArea.transform, false);

        dialogueRectRt = dialogueObj.AddComponent<RectTransform>();
        if (defaultConfig.useFrameMode)
        {
            // Mode frame : positionnement sous le speaker, avec marge
            // Calcul du padding total (frame + textContent)
            float dlgTotalPadL = defaultConfig.framePaddingLeft + defaultConfig.textContentPaddingLeft;
            float dlgTotalPadR = defaultConfig.framePaddingRight + defaultConfig.textContentPaddingRight;
            float dlgTotalPadT = defaultConfig.framePaddingTop + defaultConfig.textContentPaddingTop;
            float dlgTotalPadB = defaultConfig.framePaddingBottom + defaultConfig.textContentPaddingBottom;
            
            dialogueRectRt.anchorMin = new Vector2(0, 0f);
            dialogueRectRt.anchorMax = new Vector2(1, 1f);
            dialogueRectRt.offsetMin = new Vector2(dlgTotalPadL, dlgTotalPadB + 40); // +40 pour l'indicateur
            dialogueRectRt.offsetMax = new Vector2(-dlgTotalPadR, -dlgTotalPadT - 50 - defaultConfig.speakerMarginBottom);
        }
        else
        {
            dialogueRectRt.anchorMin = new Vector2(0, 0.18f);
            dialogueRectRt.anchorMax = new Vector2(1, 0.66f);
            dialogueRectRt.offsetMin = new Vector2(24, 0);
            dialogueRectRt.offsetMax = new Vector2(-24, 0);
        }

        dialogueText = dialogueObj.AddComponent<TextMeshProUGUI>();
        dialogueText.text = "";
        dialogueText.fontSize = defaultConfig.dialogueTextSize;
        if (ColorUtility.TryParseHtmlString(defaultConfig.dialogueTextColor, out Color txtCol))
            dialogueText.color = txtCol;
        else
            dialogueText.color = new Color(0.29f, 0.29f, 0.29f, 1f); // #4a4a4a gris foncé
        dialogueText.alignment = TextAlignmentOptions.TopLeft;

        // Indicateur de continuation (image PNG en bas au centre pour mode frame, texte pour mode legacy)
        GameObject skipObj = new GameObject("Instructions");
        skipObj.transform.SetParent(textArea.transform, false);

        instructionsRectRt = skipObj.AddComponent<RectTransform>();
        if (defaultConfig.useFrameMode)
        {
            // Mode frame : centré en bas
            instructionsRectRt.anchorMin = new Vector2(0.5f, 0f);
            instructionsRectRt.anchorMax = new Vector2(0.5f, 0f);
            instructionsRectRt.pivot = new Vector2(0.5f, 0f);
            instructionsRectRt.anchoredPosition = new Vector2(0, defaultConfig.continueIndicatorBottomMargin);
            instructionsRectRt.sizeDelta = new Vector2(60, 60); // Taille pour l'image PNG
            
            Debug.Log("[DialoguePlayer] 🎨 MODE FRAME: Création de l'Image PNG pour l'indicateur");
            
            // Créer l'Image pour le PNG
            continueIndicatorImage = skipObj.AddComponent<Image>();
            continueIndicatorImage.preserveAspect = true;
            continueIndicatorImage.raycastTarget = false;
            continueIndicatorImage.color = Color.white; // S'assurer que l'image est visible
            
            // Charger le PNG depuis uiPath
            if (GeneralConfigManager.Instance != null)
            {
                string pngUrl = GeneralConfigManager.Instance.GetUIUrl("dialogue_next.png");
                if (_continueIndicatorLoadCoroutine != null) StopCoroutine(_continueIndicatorLoadCoroutine);
                _continueIndicatorLoadCoroutine = StartCoroutine(LoadContinueIndicatorImage(pngUrl));
                Debug.Log($"[DialoguePlayer] 📥 Chargement de l'indicateur PNG depuis: {pngUrl}");
            }
            else
            {
                Debug.LogError("[DialoguePlayer] ❌ GeneralConfigManager.Instance est null!");
            }
            
            continueIndicatorImage.gameObject.SetActive(defaultConfig.showContinueIndicator);
            Debug.Log($"[DialoguePlayer] ✅ Image PNG créée, active={defaultConfig.showContinueIndicator}");
            
            // NE PAS créer de TextMeshProUGUI en mode frame
            skipInstructionText = null;
        }
        else
        {
            Debug.Log("[DialoguePlayer] 📝 MODE LEGACY: Création du TextMeshProUGUI pour l'indicateur");
            
            instructionsRectRt.anchorMin = new Vector2(0, 0.0f);
            instructionsRectRt.anchorMax = new Vector2(1, 0.18f);
            instructionsRectRt.offsetMin = new Vector2(24, 0);
            instructionsRectRt.offsetMax = new Vector2(-24, 0);
            
            // Mode legacy : créer le TextMeshProUGUI
            skipInstructionText = skipObj.AddComponent<TextMeshProUGUI>();
            skipInstructionText.text = defaultConfig.instructionsText;
            skipInstructionText.fontSize = 20f;
            skipInstructionText.color = new Color(1, 1, 1, 0.85f);
            skipInstructionText.alignment = TextAlignmentOptions.BottomRight;
            skipInstructionText.gameObject.SetActive(defaultConfig.showContinueIndicator);
            
            // NE PAS créer d'Image en mode legacy
            continueIndicatorImage = null;
        }

        dialoguePanel = panel;
        dialoguePanel.SetActive(false);

        // Créer l'image d'illustration avec le cercle rouge si elle n'est pas assignée
        if (illustrationImage == null)
        {
            // Utiliser la méthode avec cercle rouge
            SetupIllustrationCircleContainer(panel.transform, null);
            
            // Position par défaut (sera mise à jour lors de l'affichage)
            if (_illustrationContainer != null)
            {
                RectTransform containerRect = _illustrationContainer.GetComponent<RectTransform>();
                
                // Utiliser le nouveau système de positionnement relatif au panneau si disponible
                if (defaultConfig?.illustrationOffsetFromPanel != null)
                {
                    containerRect.anchorMin = new Vector2(1f, 1f);
                    containerRect.anchorMax = new Vector2(1f, 1f);
                    containerRect.pivot = new Vector2(0.5f, 0.5f);
                    
                    float offsetRight = defaultConfig.illustrationOffsetFromPanel.offsetRight;
                    float offsetTop = defaultConfig.illustrationOffsetFromPanel.offsetTop;
                    containerRect.anchoredPosition = new Vector2(-offsetRight, offsetTop);
                }
                else
                {
                    // Ancien système de positionnement absolu (rétro-compatibilité)
                    containerRect.anchorMin = new Vector2(0.5f, 0.5f);
                    containerRect.anchorMax = new Vector2(0.5f, 0.5f);
                    containerRect.pivot = new Vector2(0.5f, 0.5f);
                    
                    float posX = defaultConfig?.illustrationPosition?.x ?? 1465f;
                    float posY = defaultConfig?.illustrationPosition?.y ?? 670f;
                    float anchoredX = posX - 960f;
                    float anchoredY = posY - 540f;
                    containerRect.anchoredPosition = new Vector2(anchoredX, anchoredY);
                }
            }
            
            Debug.Log($"[DialoguePlayer] ✅ IllustrationImage créée dynamiquement AVEC cercle rouge");
        }

        // Crée le RawImage plein écran pour la vidéo du dialogue (derrière la bande)
        EnsureBackgroundUI();

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

    // ─────────────────────────────────────────────────────────────────────────
    // Lecture & jeu d'un DialogueSequence depuis une URL (JSON au format DialogueWrapper)
    // ─────────────────────────────────────────────────────────────────────────
public void PlayDialogueFromUrl(string url, DialogueConfig overrideConfig = null, Action onCompleted = null)
{
    Debug.Log($"[DialoguePlayer] Fetching dialogue JSON: {url}");
    _playToken++;
    if (_playFromUrlCoroutine != null) StopCoroutine(_playFromUrlCoroutine);
    _playFromUrlCoroutine = StartCoroutine(CoPlayDialogueFromUrl(url, overrideConfig, onCompleted));
}

private IEnumerator CoPlayDialogueFromUrl(string url, DialogueConfig overrideConfig, Action onCompleted)
{
    int token = _playToken;
    if (string.IsNullOrEmpty(url))
    {
        Debug.LogWarning("DialoguePlayer: URL vide");
        onCompleted?.Invoke();
        yield break;
    }

    DialogueSequence seq = null;
    string imgRoot = null;
    string vRoot   = null;
    DialogueConfig cfgFromJson = null;

    using (var www = UnityWebRequest.Get(url))
    {
        // NOTE: Headers Cache-Control, Pragma, Expires retirés pour éviter les problèmes CORS en WebGL
        // Ces headers ne sont pas autorisés par certains serveurs dans Access-Control-Allow-Headers

        yield return www.SendWebRequest();
        if (token != _playToken) yield break;

#if UNITY_2020_3_OR_NEWER
        if (www.result != UnityWebRequest.Result.Success)
#else
        if (www.isNetworkError || www.isHttpError)
#endif
        {
            Debug.LogError($"DialoguePlayer: GET {url} failed -> {www.error}");
            onCompleted?.Invoke();
            yield break;
        }

        string json = www.downloadHandler.text;
        try
        {
            var wrap = JsonUtility.FromJson<DialogueWrapper>(json);
            seq         = wrap != null ? wrap.dialogue       : null;
            imgRoot     = wrap != null ? wrap.imageRoot      : null;
            vRoot       = wrap != null ? wrap.videoRoot      : null;
            cfgFromJson = wrap != null ? wrap.dialogueConfig : null;

            Debug.Log($"[DialoguePlayer] JSON parsed → title='{seq?.title}', lines={(seq?.lines?.Count ?? 0)}, vRoot='{vRoot}', imgRoot='{imgRoot}', hasCfg={(cfgFromJson!=null)}");
        }
        catch (Exception e)
        {
            Debug.LogError($"DialoguePlayer: JSON parse error -> {e.Message}");
        }
    }
    if (token != _playToken) yield break;

    // Appliquer les roots si fournis par le JSON
    if (!string.IsNullOrEmpty(imgRoot))
        SetMediaRoots(imgRoot);
    if (!string.IsNullOrEmpty(vRoot))
        _videoRoot = vRoot; // prioriser le root du JSON si présent

    if (seq == null || seq.lines == null || seq.lines.Count == 0)
    {
        Debug.LogWarning($"DialoguePlayer: dialogue vide depuis {url}");
        onCompleted?.Invoke();
        yield break;
    }

    // Construire une URL vidéo absolue si possible
    if (!string.IsNullOrEmpty(seq.video))
    {
        string fullVideoUrl = seq.video;

        if (!IsAbsoluteUrl(fullVideoUrl) && !string.IsNullOrEmpty(_videoRoot))
            fullVideoUrl = CombineUrl(_videoRoot, fullVideoUrl);

        if (!IsAbsoluteUrl(fullVideoUrl))
        {
            Debug.LogWarning($"[DialoguePlayer] Aucune URL vidéo absolue construite (video='{seq.video}', videoRoot='{_videoRoot}'). On ignore la vidéo.");
            StopBackgroundVideo();
        }
        else
        {
            Debug.Log($"[DialoguePlayer] Playing background video: {fullVideoUrl}");
            yield return StartCoroutine(PlayBackgroundVideo(fullVideoUrl));
        }
    }
    else
    {
        StopBackgroundVideo();
    }

    // (Optionnel) amener l'UI au premier plan si la méthode existe
    try { BringToFront(50000); } catch {}

    // Brancher les callbacks de fin/skip pour libérer l’appelant
    // IMPORTANT: ne pas écraser les delegates existants (évite des courses + handlers perdus).
    Action oneShot = null;
    oneShot = () =>
    {
        try { onCompleted?.Invoke(); } catch { /* ignore */ }
        OnDialogueComplete -= oneShot;
        OnDialogueSkipped  -= oneShot;
    };
    OnDialogueComplete += oneShot;
    OnDialogueSkipped  += oneShot;

    // ⚠️ Priorité à la config GLOBALE (overrideConfig), sinon on prend celle du JSON
    PlayDialogue(seq, overrideConfig ?? cfgFromJson);
    yield break;
}







private IEnumerator PlayBackgroundVideo(string fullUrl)
{
    EnsureBackgroundUI();

    if (_bgVideoPlayer == null)
    {
        var go = new GameObject("DialogueVideoPlayer");
        go.transform.SetParent(this.transform, false);
        _bgVideoPlayer = go.AddComponent<VideoPlayer>();
        _bgVideoPlayer.playOnAwake = false;
        _bgVideoPlayer.isLooping = true;
        
        // FIX MAC : Utiliser CameraNearPlane sur Mac pour éviter le voile
        // Les vidéos de fond utilisent CameraNearPlane et fonctionnent bien
        if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer)
        {
            _bgVideoPlayer.renderMode = VideoRenderMode.CameraNearPlane;
            Camera mainCam = Camera.main;
            if (mainCam != null)
            {
                _bgVideoPlayer.targetCamera = mainCam;
                _bgVideoPlayer.targetCameraAlpha = 1f;
            }
            Debug.Log("[DialoguePlayer] 🍎 Mac détecté - utilisation de CameraNearPlane pour la vidéo");
        }
        else
        {
            _bgVideoPlayer.renderMode = VideoRenderMode.RenderTexture;
        }
        _bgVideoPlayer.audioOutputMode = VideoAudioOutputMode.None;
    }

    // FIX MAC : Sur Mac, utiliser CameraNearPlane (pas besoin de RenderTexture)
    if (_bgVideoPlayer.renderMode == VideoRenderMode.CameraNearPlane)
    {
        // Sur Mac avec CameraNearPlane, la vidéo est rendue directement sur la caméra
        // Pas besoin de RenderTexture ni de RawImage
        if (videoBackgroundUI != null)
        {
            videoBackgroundUI.enabled = false;
            videoBackgroundUI.gameObject.SetActive(false);
        }
        Debug.Log("[DialoguePlayer] 🍎 Mode CameraNearPlane - vidéo rendue directement sur la caméra");
    }
    else
    {
        // Mode RenderTexture pour les autres plateformes
        // RT (recréée si taille écran a changé)
        int w = Mathf.Max(8, Screen.width);
        int h = Mathf.Max(8, Screen.height);
        if (_bgRT == null || _bgRT.width != w || _bgRT.height != h)
        {
            if (_bgRT != null) _bgRT.Release();
            _bgRT = new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32) { name = "Dialogue_RT" };
            _bgRT.Create();
        }

        _bgVideoPlayer.targetTexture = _bgRT;

        // Affichage RawImage
        if (videoBackgroundUI == null) EnsureBackgroundUI();
        if (videoBackgroundUI != null)
        {
            videoBackgroundUI.texture = _bgRT;
            videoBackgroundUI.transform.SetAsFirstSibling();
            videoBackgroundUI.enabled = true;
            videoBackgroundUI.gameObject.SetActive(true);
            Debug.Log($"[DialoguePlayer] ✅ Vidéo activée - RawImage enabled: {videoBackgroundUI.enabled}, active: {videoBackgroundUI.gameObject.activeSelf}, texture: {(_bgRT != null ? "OK" : "NULL")}");
        }
        else
        {
            Debug.LogError("[DialoguePlayer] ❌ videoBackgroundUI est null après EnsureBackgroundUI()");
        }
    }

    // Tant que la vidéo n’est pas prête, on masque le jeu par un noir opaque
    if (backgroundImage != null)
    {
        var c = backgroundImage.color; c.a = 1f; backgroundImage.color = c;
    }

    _bgVideoPlayer.url = fullUrl;
    
    // FIX MAC : S'assurer que la caméra est bien configurée si on utilise CameraNearPlane
    if (_bgVideoPlayer.renderMode == VideoRenderMode.CameraNearPlane)
    {
        Camera mainCam = Camera.main;
        if (mainCam != null && _bgVideoPlayer.targetCamera != mainCam)
        {
            _bgVideoPlayer.targetCamera = mainCam;
            _bgVideoPlayer.targetCameraAlpha = 1f;
            Debug.Log($"[DialoguePlayer] 🍎 Caméra configurée: {mainCam.name}, alpha={_bgVideoPlayer.targetCameraAlpha}");
        }
    }
    
    _bgVideoPlayer.Prepare();
    while (!_bgVideoPlayer.isPrepared) yield return null;

    _bgVideoPlayer.Play();

    // Vidéo prête → appliquer le dimming voulu
    if (backgroundImage != null)
    {
        var c = backgroundImage.color;
        bool isCameraPlane = (_bgVideoPlayer.renderMode == VideoRenderMode.CameraNearPlane);

        if (isCameraPlane)
        {
            // Sur Mac (CameraNearPlane), la vidéo est derrière l'UI.
            // On coupe complètement le voile pour éviter l'écran blanc.
            c.a = 0f;
            backgroundImage.color = c;
            backgroundImage.raycastTarget = false;
            backgroundImage.enabled = false;
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
            DisableFullScreenVeilsForMac();
#endif
        }
        else
        {
            backgroundImage.enabled = true;
            backgroundImage.raycastTarget = true;
            c.a = Mathf.Clamp01(currentConfig != null ? currentConfig.backgroundDimming : 0.25f);
            backgroundImage.color = c;
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
            RestoreFullScreenVeilsForMac();
#endif
        }
    }
}

private void StopBackgroundVideo()
{
    if (_bgVideoPlayer != null) _bgVideoPlayer.Stop();

    if (videoBackgroundUI != null)
    {
        videoBackgroundUI.texture = null;
        // 🔒 Failsafe anti-cadre-blanc : RawImage désactivé s'il n'a pas de texture
        videoBackgroundUI.gameObject.SetActive(false);
        videoBackgroundUI.enabled = false;
    }

    // WebGL / GPU: libérer la RenderTexture dès que possible
    if (_bgRT != null)
    {
        _bgRT.Release();
        _bgRT = null;
    }

    // Réactiver le voile si on revient à une image fixe (utile pour masquer le jeu)
    if (backgroundImage != null)
    {
        backgroundImage.enabled = true;
        backgroundImage.raycastTarget = true;
        var c = backgroundImage.color;
        c.a = 1f;
        backgroundImage.color = c;
    }

#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
    RestoreFullScreenVeilsForMac();
#endif
}


#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
private void DisableFullScreenVeilsForMac()
{
    if (dialoguePanel == null) return;

    var images = dialoguePanel.GetComponentsInChildren<Image>(true);
    foreach (var img in images)
    {
        if (img == null) continue;
        if (img == overlayImage || img == illustrationImage || img == bottomBarBg) continue;
        if (videoBackgroundUI != null && img.gameObject == videoBackgroundUI.gameObject) continue;
        if (img.sprite != null) continue;

        RectTransform rt = img.rectTransform;
        if (rt == null) continue;

        bool fullWidth = Mathf.Abs(rt.anchorMin.x) < 0.0001f && Mathf.Abs(rt.anchorMax.x - 1f) < 0.0001f;
        bool fullHeight = Mathf.Abs(rt.anchorMin.y) < 0.0001f && Mathf.Abs(rt.anchorMax.y - 1f) < 0.0001f;
        bool zeroOffsets = rt.offsetMin.sqrMagnitude < 0.01f && rt.offsetMax.sqrMagnitude < 0.01f;

        if (!fullWidth || !fullHeight || !zeroOffsets) continue;
        if (img.color.a <= 0.001f) continue;

        bool alreadyTracked = _macHiddenImages.Exists(state => state.image == img);
        if (!alreadyTracked)
        {
            _macHiddenImages.Add(new MacHiddenImageState
            {
                image = img,
                color = img.color,
                enabled = img.enabled,
                raycastTarget = img.raycastTarget
            });
        }

        img.enabled = false;
        img.raycastTarget = false;
        Color c = img.color;
        c.a = 0f;
        img.color = c;
    }
}

private void RestoreFullScreenVeilsForMac()
{
    if (_macHiddenImages.Count == 0) return;

    foreach (var state in _macHiddenImages)
    {
        if (state.image == null) continue;
        state.image.color = state.color;
        state.image.enabled = state.enabled;
        state.image.raycastTarget = state.raycastTarget;
    }

    _macHiddenImages.Clear();
}
#endif



    // ─────────────────────────────────────────────────────────────────────────
    // Input helpers
    // ─────────────────────────────────────────────────────────────────────────
    private string SkipKeyName()
    {
        if (currentConfig != null && !string.IsNullOrEmpty(currentConfig.skipDialogueKey))
            return currentConfig.skipDialogueKey;
        return "Escape";
    }

    // Helper d'input UNIQUE (New Input System + fallback legacy)
    private bool GetKeyPressedThisFrame(string keyName)
    {
        if (string.IsNullOrEmpty(keyName)) return false;

    #if ENABLE_INPUT_SYSTEM
        // New Input System
        if (Keyboard.current == null) return false;

        // Aliases usuels
        if (keyName.Equals("Enter", StringComparison.OrdinalIgnoreCase) ||
            keyName.Equals("Return", StringComparison.OrdinalIgnoreCase))
            return Keyboard.current.enterKey.wasPressedThisFrame
                || Keyboard.current.numpadEnterKey.wasPressedThisFrame;

        if (keyName.Equals("Space", StringComparison.OrdinalIgnoreCase))
            return Keyboard.current.spaceKey.wasPressedThisFrame;

        if (keyName.Equals("Backspace", StringComparison.OrdinalIgnoreCase))
            return Keyboard.current.backspaceKey.wasPressedThisFrame;

        // Tentative générique via l’enum Key
        if (Enum.TryParse<Key>(keyName, true, out var k))
        {
            var ctrl = Keyboard.current[k];
            return (ctrl != null) && ctrl.wasPressedThisFrame;
        }
        return false;
    #else
        // Legacy Input Manager
        if (keyName.Equals("Enter", StringComparison.OrdinalIgnoreCase))
            keyName = "Return";

        if (!Enum.TryParse<KeyCode>(keyName, true, out var kc))
            return false;

        return Input.GetKeyDown(kc);
    #endif
    }

    private bool GetSkipPressedThisFrame() => GetKeyPressedThisFrame(SkipKeyName());
    private bool GetNextPressedThisFrame() => GetKeyPressedThisFrame(currentConfig?.nextKey ?? "Space");
    private bool GetPrevPressedThisFrame() => GetKeyPressedThisFrame(currentConfig?.prevKey ?? "Backspace");

    void HandleInput()
    {
        if (!isPlaying) return;

        // Skip
        if (GetKeyPressedThisFrame(currentConfig?.skipDialogueKey ?? "Escape"))
        {
            if (isWaitingForInput) NextLine();
            else SkipDialogue();
            return;
        }

        // Navigation
        bool next = GetKeyPressedThisFrame(currentConfig?.nextKey ?? "Space");
        bool prev = GetKeyPressedThisFrame(currentConfig?.prevKey ?? "Backspace");

        // Click / Touch = Next
    #if ENABLE_INPUT_SYSTEM
        if (!next)
        {
            if (Mouse.current != null && Mouse.current.leftButton.wasPressedThisFrame) next = true;
            if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.wasPressedThisFrame) next = true;
            
            // WebGL fallback
            #if UNITY_WEBGL && !UNITY_EDITOR
            if (WebGLClickReceiver.WasClickedThisFrame()) 
            {
                next = true;
                Debug.Log("[DialoguePlayer] WasClickedThisFrame detected in Update");
            }
            #endif
        }
    #else
        if (!next && Input.GetMouseButtonDown(0)) next = true;
    #endif

        if (isWaitingForInput)
        {
            if (next)      NextLine();
            else if (prev) PrevLine();
        }
    }
    
    /// <summary>
    /// Appelé par WebGLClickReceiver quand un clic est reçu depuis JavaScript
    /// </summary>
    public void OnWebGLClick()
    {
        if (!isPlaying) return;
        
        // DEBUG : Vérifier si l'appel arrive
        Debug.Log($"[DialoguePlayer] OnWebGLClick reçu. isWaitingForInput={isWaitingForInput}");
        
        // Si on attend une entrée, passer à la ligne suivante
        if (isWaitingForInput)
        {
            NextLine();
        }
    }


    public void BringToFront(int order = 50000)
    {
        if (dialoguePanel == null) return;

        var cnv = dialoguePanel.GetComponent<Canvas>();
        if (cnv == null) cnv = dialoguePanel.AddComponent<Canvas>();
        cnv.renderMode = RenderMode.ScreenSpaceOverlay; // toujours au-dessus
        cnv.overrideSorting = true;
        cnv.sortingOrder = order;

        // AJOUT : Forcer la résolution 1920x1080 via CanvasScaler
        var scaler = dialoguePanel.GetComponent<CanvasScaler>();
        if (scaler == null) scaler = dialoguePanel.AddComponent<CanvasScaler>();
        scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        scaler.referenceResolution = new Vector2(1920, 1080);
        scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
        scaler.matchWidthOrHeight = 1.0f; // Privilégier la hauteur ou 0.5f pour l'équilibre

        dialoguePanel.transform.SetAsLastSibling();
    }

private void ForceShowPanel()
{
    if (dialoguePanel == null) return;

    var cg = dialoguePanel.GetComponent<CanvasGroup>();
    if (cg == null) cg = dialoguePanel.AddComponent<CanvasGroup>();
    dialoguePanel.SetActive(true);
    cg.alpha = 1f;

    // 🔧 CORRECTION : S'assurer que le bottomBar est également activé
    if (bottomBarRect != null)
    {
        bottomBarRect.gameObject.SetActive(true);
    }
    if (bottomBarBg != null)
    {
        bottomBarBg.gameObject.SetActive(true);
        bottomBarBg.enabled = true;
    }
    if (dialogueText != null)
    {
        dialogueText.gameObject.SetActive(true);
        dialogueText.enabled = true;
    }
    if (speakerText != null)
    {
        speakerText.gameObject.SetActive(true);
        speakerText.enabled = true;
    }

    BringToFront(50000);
    Debug.Log($"[DialoguePlayer] ForceShowPanel: active={dialoguePanel.activeInHierarchy}, sort={dialoguePanel.GetComponent<Canvas>()?.sortingOrder}");
}




// Coupe tout rendu "vide" susceptible d'apparaître blanc
private void DisableEmptyUIRenderers()
{
    // 1) RawImage (vidéo)
    if (videoBackgroundUI != null)
    {
        bool hasTex = videoBackgroundUI.texture != null;
        videoBackgroundUI.enabled = hasTex;
        videoBackgroundUI.gameObject.SetActive(hasTex);
        if (!hasTex)
        {
            // met la couleur totalement transparente pour éviter toute coloration blanche résiduelle
            var c = videoBackgroundUI.color; c.a = 0f; videoBackgroundUI.color = c;
        }
        videoBackgroundUI.raycastTarget = false;
    }

    // 2) Overlay (Image)
    if (overlayImage != null)
    {
        bool hasSprite = overlayImage.sprite != null;
        overlayImage.enabled = hasSprite;
        overlayImage.gameObject.SetActive(hasSprite);
        if (!hasSprite)
        {
            var c = overlayImage.color; c.a = 0f; overlayImage.color = c;
        }
        overlayImage.raycastTarget = false;
    }

    // 3) Illustration (Image)
    if (illustrationImage != null)
    {
        bool hasSprite = illustrationImage.sprite != null;
        illustrationImage.enabled = hasSprite;
        if (_illustrationCg != null) _illustrationCg.alpha = hasSprite ? 1f : 0f;
        illustrationImage.raycastTarget = false;
    }
}

// Appelle ceci juste avant d'afficher une ligne, et aussi quand tu quittes le dialogue
private void HardCleanupVisuals()
{
    // Nettoie l'état interne puis coupe tout
    if (overlayImage != null) { overlayImage.sprite = null; }
    if (illustrationImage != null) { illustrationImage.sprite = null; }
    if (videoBackgroundUI != null) { videoBackgroundUI.texture = null; }

    DisableEmptyUIRenderers();
}




public void ForceCleanupAndHide()
{
    // Détruire les ressources runtime d'illustrations (grosses fuites en WebGL si on ne les libère pas)
    try
    {
        if (dynamicIllustrationSprites != null)
        {
            foreach (var sp in dynamicIllustrationSprites)
            {
                if (sp != null) Destroy(sp);
            }
            dynamicIllustrationSprites.Clear();
        }

        if (dynamicIllustrationTextures != null)
        {
            foreach (var tx in dynamicIllustrationTextures)
            {
                if (tx != null) Destroy(tx);
            }
            dynamicIllustrationTextures.Clear();
        }
    }
    catch { /* ignore */ }

    // Détruire aussi les sprites/tex téléchargés non trackés explicitement
    try
    {
        if (overlayImage != null && overlayImage.sprite != null)
        {
            var tex = overlayImage.sprite.texture;
            Destroy(overlayImage.sprite);
            if (tex != null) Destroy(tex);
        }
        if (continueIndicatorImage != null && continueIndicatorImage.sprite != null)
        {
            var tex = continueIndicatorImage.sprite.texture;
            Destroy(continueIndicatorImage.sprite);
            if (tex != null) Destroy(tex);
        }
    }
    catch { /* ignore */ }

    // Arrêter toute vidéo
    if (_bgVideoPlayer != null)
    {
        _bgVideoPlayer.Stop();
        if (_bgVideoPlayer.gameObject != null)
            _bgVideoPlayer.gameObject.SetActive(false);
    }
    
    // Nettoyer la RenderTexture
    if (_bgRT != null)
    {
        _bgRT.Release();
        _bgRT = null;
    }

    // Détruire le VideoPlayer runtime pour libérer le heap WebGL (il sera recréé si besoin)
    try
    {
        if (_bgVideoPlayer != null && _bgVideoPlayer.gameObject != null)
        {
            Destroy(_bgVideoPlayer.gameObject);
        }
    }
    catch { /* ignore */ }
    _bgVideoPlayer = null;
    
    // Forcer le RawImage vidéo à être complètement inactif
    if (videoBackgroundUI != null)
    {
        videoBackgroundUI.texture = null;
        videoBackgroundUI.enabled = false;
        videoBackgroundUI.gameObject.SetActive(false);
        var c = videoBackgroundUI.color; c.a = 0f; videoBackgroundUI.color = c;
    }
    
    // Nettoyer les images
    if (overlayImage != null)
    {
        overlayImage.sprite = null;
        overlayImage.enabled = false;
        overlayImage.gameObject.SetActive(false);
    }
    
    if (illustrationImage != null)
    {
        illustrationImage.sprite = null;
        illustrationImage.enabled = false;
        if (_illustrationCg != null) _illustrationCg.alpha = 0f;
    }
    
    // Désactiver complètement le panel
    if (dialoguePanel != null)
    {
        var canvas = dialoguePanel.GetComponent<Canvas>();
        if (canvas != null) canvas.sortingOrder = -1;
        dialoguePanel.SetActive(false);
    }
    
    // Réinitialiser l'état
    isPlaying = false;
    isWaitingForInput = false;
}




}
