using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using UnityEngine.Networking;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using TMPro;

/// <summary>
/// Construit dynamiquement le panneau de prévisualisation de quête
/// </summary>
public class QuestPreviewPanelBuilder : MonoBehaviour
{
    public static QuestPreviewPanelBuilder Instance { get; private set; }
    
    private Canvas parentCanvas;
    private GameObject panelRoot;
    private QuestPreviewPanelConfig config;
    private TMP_FontAsset titleFont;
    private TMP_FontAsset textFont;
    private Sprite closeButtonSprite;
    private Sprite headerIconSprite; // Icône horloge pour le header
    
    // Liste pour tracker les ressources créées dynamiquement et les nettoyer
    // IMPORTANT: en WebGL, il faut nettoyer à CHAQUE rebuild, sinon OOM.
    private readonly List<Texture2D> dynamicTextures = new List<Texture2D>();
    private readonly List<Sprite> dynamicSprites = new List<Sprite>();
    private readonly List<RenderTexture> dynamicRenderTextures = new List<RenderTexture>();

    // Ressources "cachées" (chargées une fois, réutilisées). On ne les détruit pas à chaque rebuild.
    private Sprite cachedCloseButtonSprite;
    private Texture2D cachedCloseButtonTexture;
    private Sprite cachedHeaderIconSprite;
    private Texture2D cachedHeaderIconTexture;
    private Sprite cachedFormationArrowSprite;
    private Texture2D cachedFormationArrowTexture;
    private Sprite cachedBadgeDashboardSprite;
    private Texture2D cachedBadgeDashboardTexture;
    private bool isLoadingFormationArrowSprite = false;
    private readonly List<Image> pendingFormationArrowTargets = new List<Image>();

    // Vidéo: il faut explicitement stopper + libérer la RenderTexture.
    private VideoPlayer currentVideoPlayer;
    private RenderTexture currentVideoRenderTexture;
    private RawImage currentVideoRawImage;

    private Coroutine buildRoutine;
    private int buildGeneration = 0;

    // Vrai tant qu'on souhaite que le panel preview soit affiché (utile pour gérer la visibilité du bouton Options)
    private bool previewOpenDesired = false;

    private void SetOptionsButtonVisible(bool visible)
    {
        try { HeaderManager.Instance?.SetOptionsButtonVisible(visible); } catch { /* ignore */ }
    }
    
    // Données de la quête actuelle
    private string currentQuestId;
    private int currentQuestNumericId; // ID numérique pour les appels API
    private string currentQuestTitle;
    private string currentQuestDescription;
    private string currentQuestDuration;
    private string currentVideoUrl;
    private string[] currentFormations;
    private List<Training> currentTrainings; // Données complètes avec URLs
    private List<QuestUserProgress> currentUserProgress; // Progression utilisateur par difficulté
    private bool currentQuestIsDda; // Si true, affiche l'icône horloge dans le header
    private bool isNextQuestAvailable; // Si true, la quête suivante est déjà disponible (pas de bouton "Passer")
    private List<Quest> allQuestsData; // Données de toutes les quêtes pour vérifier les parties en cours
    private Action onStartQuest;
    private Action onSkipQuest;
    
    // Références UI
    private List<Button> difficultyButtons = new List<Button>();
    // Indique si chaque bouton de difficulté correspond à un niveau complété (badge)
    private readonly List<bool> difficultyIsCompleted = new List<bool>();
    private int selectedDifficultyIndex = -1; // -1 = aucune sélection
    private int inProgressDifficultyIndex = -1; // Index de la difficulté en cours (completion > 0 et < 100)
    private Button startButton;
    private Image startButtonImage;
    private Sprite startButtonEnabledSprite;
    private Sprite startButtonDisabledSprite;
    private TextMeshProUGUI startButtonText;
    private TextMeshProUGUI warningText; // Texte d'avertissement au-dessus du bouton
    
    // Modale de confirmation pour changement de difficulté
    private GameObject confirmationModal;
    private string pendingConfirmationMessage; // Message à afficher dans la modale
    
    // Flag pour logs verbeux (désactivé en production)
    // (static readonly pour éviter CS0162 "unreachable code detected" quand le compilateur voit une constante)
    private static readonly bool VERBOSE_DEBUG = false;
    
    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    private void LogVerbose(string message)
    {
        if (VERBOSE_DEBUG) Debug.Log(message);
    }
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else
        {
            Destroy(gameObject);
        }

        // ✅ Point de nettoyage global: si un changement de scène survient pendant que le panel existe,
        // on nettoie explicitement ses ressources runtime (prévention WebGL OOM).
        CleanupPoints.Instance?.RegisterOnSceneChange("QuestPreviewPanelBuilder", () =>
        {
            // Si on change de scène pendant que le panel est ouvert, restaurer le bouton Options
            previewOpenDesired = false;
            SetOptionsButtonVisible(true);
            CleanupRuntimeResources(destroyPanelRoot: true);
        });
    }
    
    public void SetParentCanvas(Canvas canvas)
    {
        parentCanvas = canvas;
        LogVerbose($"[QuestPreviewPanel] Canvas parent défini: {canvas.name}");
    }
    
    /// <summary>
    /// Affiche le panel avec les données d'une quête depuis l'API
    /// </summary>
    /// <param name="mapId">ID de la map (ex: "map-Q0")</param>
    /// <param name="questData">Données de la quête</param>
    /// <param name="onStart">Action appelée quand on démarre la quête</param>
    /// <param name="onSkip">Action appelée quand on passe la quête</param>
    /// <param name="nextQuestAvailable">Si true, la quête suivante est déjà disponible - le bouton "Passer" sera masqué</param>
    /// <param name="allQuests">Liste de toutes les quêtes pour vérifier si une autre partie est en cours</param>
    public void ShowQuestPreviewWithQuestData(string mapId, Quest questData, Action onStart, Action onSkip, bool nextQuestAvailable = false, List<Quest> allQuests = null)
    {
        LogVerbose($"[QuestPreviewPanel] 🎮 JOUER: ShowQuestPreviewWithQuestData APPELÉ - mapId={mapId}, questData null? {questData == null}, nextQuestAvailable={nextQuestAvailable}, allQuests count={allQuests?.Count ?? 0}");
        LogVerbose($"[QuestPreviewPanel] 🎮 JOUER: parentCanvas null? {parentCanvas == null}");
        
        // Si l'utilisateur clique vite (changement de quête), annuler le build précédent et libérer les ressources
        CancelBuild();
        CleanupRuntimeResources(destroyPanelRoot: true);

        // Masquer le bouton Options tant que le preview est affiché
        previewOpenDesired = true;
        SetOptionsButtonVisible(false);

        currentQuestId = mapId;
        currentQuestNumericId = questData.id; // Stocker l'ID numérique pour les appels API
        currentQuestTitle = questData.title;
        isNextQuestAvailable = nextQuestAvailable; // Stocker si la quête suivante est disponible
        allQuestsData = allQuests; // Stocker les données de toutes les quêtes
        currentQuestDescription = questData.description;
        currentQuestDuration = questData.duration;
        currentVideoUrl = questData.trailer_url;
        currentTrainings = questData.trainings; // Stocker les trainings complets avec URLs
        currentUserProgress = questData.user; // Stocker la progression utilisateur
        currentQuestIsDda = questData.dda; // Stocker si la quête est en mode DDA (affichage horloge)
        
        // Convertir les trainings en tableau de string (pour compatibilité)
        if (questData.trainings != null && questData.trainings.Count > 0)
        {
            currentFormations = new string[questData.trainings.Count];
            for (int i = 0; i < questData.trainings.Count; i++)
            {
                currentFormations[i] = questData.trainings[i].title;
            }
        }
        else
        {
            currentFormations = new string[0];
        }
        
        onStartQuest = onStart;
        onSkipQuest = onSkip;
        
        // Utiliser sprite_url ou une icône par défaut
        string iconUrl = !string.IsNullOrEmpty(questData.sprite_url) ? questData.sprite_url : "UI_quest_icon_dda.png";
        
        // Log de la progression utilisateur
        if (currentUserProgress != null && currentUserProgress.Count > 0)
        {
            LogVerbose($"[QuestPreviewPanel] Progression utilisateur pour la quête:");
            foreach (var progress in currentUserProgress)
            {
                LogVerbose($"  - {progress.difficulty}: completion={progress.completion}%, score={progress.score}, last_completed_game={progress.last_completed_game}");
            }
        }
        else
        {
            LogVerbose($"[QuestPreviewPanel] Pas de données de progression utilisateur");
        }
        
        LogVerbose($"[QuestPreviewPanel] Affichage de la quête - Titre: {currentQuestTitle}, Durée: {currentQuestDuration}, Formations: {currentFormations.Length}");
        
        LogVerbose($"[QuestPreviewPanel] 🎮 JOUER: Lancement de BuildPanelCoroutine...");
        buildRoutine = StartCoroutine(BuildPanelCoroutine(iconUrl));
        LogVerbose($"[QuestPreviewPanel] 🎮 JOUER: ✅ BuildPanelCoroutine lancée");
    }
    
    /// <summary>
    /// Affiche le panel de prévisualisation pour une quête (ancienne méthode pour compatibilité)
    /// </summary>
    public void ShowQuestPreview(string questId, string title, string description, string duration, 
        string videoUrl, string[] formations, string iconUrl, Action onStart, Action onSkip)
    {
        CancelBuild();
        CleanupRuntimeResources(destroyPanelRoot: true);

        previewOpenDesired = true;
        SetOptionsButtonVisible(false);

        currentQuestId = questId;
        currentQuestTitle = title;
        currentQuestDescription = description;
        currentQuestDuration = duration;
        currentVideoUrl = videoUrl;
        currentFormations = formations;
        onStartQuest = onStart;
        onSkipQuest = onSkip;
        
        buildRoutine = StartCoroutine(BuildPanelCoroutine(iconUrl));
    }
    
    /// <summary>
    /// Vérifie si le panel est actuellement ouvert
    /// </summary>
    public bool IsPanelOpen()
    {
        return panelRoot != null;
    }
    
    /// <summary>
    /// Ferme le panel
    /// </summary>
    public void ClosePanel()
    {
        previewOpenDesired = false;
        SetOptionsButtonVisible(true);
        CancelBuild();
        CleanupRuntimeResources(destroyPanelRoot: true);
        LogVerbose("[QuestPreviewPanel] Panel fermé et ressources nettoyées");
    }
    
    /// <summary>
    /// Nettoie les ressources quand l'objet est détruit
    /// </summary>
    void OnDestroy()
    {
        previewOpenDesired = false;
        SetOptionsButtonVisible(true);
        CancelBuild();
        CleanupRuntimeResources(destroyPanelRoot: true);

        // Se désinscrire (évite de garder une référence à un objet détruit)
        try { CleanupPoints.GetExisting()?.UnregisterOnSceneChange("QuestPreviewPanelBuilder"); } catch { }

        // Détruire aussi les ressources en cache (chargées via WebRequest)
        if (cachedCloseButtonSprite != null) Destroy(cachedCloseButtonSprite);
        if (cachedCloseButtonTexture != null) Destroy(cachedCloseButtonTexture);
        if (cachedHeaderIconSprite != null) Destroy(cachedHeaderIconSprite);
        if (cachedHeaderIconTexture != null) Destroy(cachedHeaderIconTexture);
        if (cachedFormationArrowSprite != null) Destroy(cachedFormationArrowSprite);
        if (cachedFormationArrowTexture != null) Destroy(cachedFormationArrowTexture);
        if (cachedBadgeDashboardSprite != null) Destroy(cachedBadgeDashboardSprite);
        if (cachedBadgeDashboardTexture != null) Destroy(cachedBadgeDashboardTexture);

        cachedCloseButtonSprite = null;
        cachedCloseButtonTexture = null;
        cachedHeaderIconSprite = null;
        cachedHeaderIconTexture = null;
        cachedFormationArrowSprite = null;
        cachedFormationArrowTexture = null;
        cachedBadgeDashboardSprite = null;
        cachedBadgeDashboardTexture = null;
    }
    
    IEnumerator BuildPanelCoroutine(string iconUrl)
    {
        LogVerbose($"[QuestPreviewPanel] 🎮 JOUER: BuildPanelCoroutine DÉMARRÉ - iconUrl={iconUrl}");
        
        // Invalider tout ce qui était en cours (coroutines / chargements)
        buildGeneration++;

        // Charger la configuration
        if (GeneralConfigManager.Instance == null)
        {
            Debug.LogError("[QuestPreviewPanel] 🎮 JOUER: ❌ GeneralConfigManager.Instance est null!");
            yield break;
        }
        
        var generalConfig = GeneralConfigManager.Instance.GetConfig();
        if (generalConfig == null)
        {
            Debug.LogError("[QuestPreviewPanel] ❌ GeneralConfig est null!");
            yield break;
        }
        
        config = generalConfig.questPreviewPanel;
        if (config == null)
        {
            Debug.LogError("[QuestPreviewPanel] ❌ questPreviewPanel n'est pas défini dans GeneralConfig!");
            Debug.LogError("[QuestPreviewPanel] ❌ Vérifiez que le fichier general-config.json contient bien la section 'questPreviewPanel'");
            yield break;
        }
        
        LogVerbose("[QuestPreviewPanel] ✅ Configuration chargée avec succès");

        // Sécurité: si on a demandé l'ouverture du preview, s'assurer que le bouton Options est bien masqué
        if (previewOpenDesired) SetOptionsButtonVisible(false);
        
        // Nettoyer TOUT ce qui a pu être créé lors d'une précédente sélection de quête
        // (sinon OOM WebGL en quelques clics). Ne stoppe pas le build courant.
        CleanupRuntimeResources(destroyPanelRoot: true);

        // Charger les ressources (avec cache)
        yield return StartCoroutine(LoadResources(iconUrl));
        
        // Trouver le canvas si non défini
        if (parentCanvas == null)
        {
            parentCanvas = FindFirstObjectByType<Canvas>();
            if (parentCanvas == null)
            {
                Debug.LogError("[QuestPreviewPanel] Aucun Canvas trouvé!");
                yield break;
            }
        }
        
        // À ce stade, l'ancien panel (et ses ressources) a déjà été nettoyé via CleanupRuntimeResources()
        
        // Créer le panel
        CreatePanel();
        
        LogVerbose("[QuestPreviewPanel] Panel créé avec succès");
    }
    
    IEnumerator LoadResources(string iconUrl)
    {
        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        string uiPath = generalConfig?.assetsPaths?.uiPath ?? "";
        
        // Charger les polices (comme LoginPopup : essayer plusieurs chemins)
        string titleFontName = config.header?.title?.fontName ?? "Anton-Regular SDF";
        string textFontName = config.description?.fontName ?? "Lato-Regular SDF";
        
        LogVerbose($"[QuestPreviewPanel] Tentative chargement polices - Title: '{titleFontName}', Text: '{textFontName}'");
        
        // Essayer plusieurs chemins pour la police du titre (comme LoginPopup)
        titleFont = Resources.Load<TMP_FontAsset>($"Fonts/{titleFontName}");
        if (titleFont == null)
        {
            titleFont = Resources.Load<TMP_FontAsset>($"fonts/{titleFontName}");
        }
        if (titleFont == null)
        {
            titleFont = Resources.Load<TMP_FontAsset>(titleFontName);
        }
        
        // Essayer plusieurs chemins pour la police du texte
        textFont = Resources.Load<TMP_FontAsset>($"Fonts/{textFontName}");
        if (textFont == null)
        {
            textFont = Resources.Load<TMP_FontAsset>($"fonts/{textFontName}");
        }
        if (textFont == null)
        {
            textFont = Resources.Load<TMP_FontAsset>(textFontName);
        }
        
        if (titleFont != null)
            LogVerbose($"[QuestPreviewPanel] ✅ Police titre chargée: {titleFont.name}");
        else
            Debug.LogWarning($"[QuestPreviewPanel] ❌ Police titre '{titleFontName}' non trouvée - Essayé: Fonts/{titleFontName}, fonts/{titleFontName}, {titleFontName}");
            
        if (textFont != null)
            LogVerbose($"[QuestPreviewPanel] ✅ Police texte chargée: {textFont.name}");
        else
            Debug.LogWarning($"[QuestPreviewPanel] ❌ Police texte '{textFontName}' non trouvée");
        
        // Charger le sprite du bouton fermer (cache)
        if (cachedCloseButtonSprite == null)
        {
            string closeButtonUrl = uiPath + (config.header?.closeButton?.imageUrl ?? "UI_fermer.png");
            yield return StartCoroutine(LoadRemoteSprite(closeButtonUrl, sprite =>
            {
                cachedCloseButtonSprite = sprite;
            }, tex =>
            {
                cachedCloseButtonTexture = tex;
            }));
        }
        closeButtonSprite = cachedCloseButtonSprite;

        // Charger l'icône horloge pour le header (cache)
        if (cachedHeaderIconSprite == null)
        {
            string headerIconUrl = uiPath + "horloge-dda.png";
            yield return StartCoroutine(LoadRemoteSprite(headerIconUrl, sprite =>
            {
                cachedHeaderIconSprite = sprite;
            }, tex =>
            {
                cachedHeaderIconTexture = tex;
            }));
        }
        headerIconSprite = cachedHeaderIconSprite;

        // Charger le badge dashboard (utilisé dans les boutons de difficulté complétés)
        if (cachedBadgeDashboardSprite == null)
        {
            string badgeDashboardUrl = uiPath + "badge_dashboard.png";
            yield return StartCoroutine(LoadRemoteSprite(badgeDashboardUrl, sprite =>
            {
                cachedBadgeDashboardSprite = sprite;
            }, tex =>
            {
                cachedBadgeDashboardTexture = tex;
            }));
        }
    }
    
    IEnumerator LoadRemoteSprite(string url, Action<Sprite> onLoaded, Action<Texture2D> onTextureLoaded)
    {
        using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url))
        {
            yield return request.SendWebRequest();
            
            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), 100f);
                // IMPORTANT: ces textures/sprites doivent être détruits explicitement, sinon fuite mémoire WebGL.
                // Ici on les met en cache, donc on ne les tracke pas dans dynamicTextures/dynamicSprites.
                onTextureLoaded?.Invoke(texture);
                onLoaded?.Invoke(sprite);
            }
            else
            {
                Debug.LogWarning($"[QuestPreviewPanel] Échec du chargement: {url}");
            }
        }
    }
    
    IEnumerator LoadFormationArrowSprite(Image targetImage)
    {
        if (targetImage == null) yield break;

        // Cache global (chargé une fois), + support de multiples cibles pendant le chargement
        if (cachedFormationArrowSprite != null)
        {
            targetImage.sprite = cachedFormationArrowSprite;
            yield break;
        }

        pendingFormationArrowTargets.Add(targetImage);
        if (isLoadingFormationArrowSprite) yield break;

        isLoadingFormationArrowSprite = true;

        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        string uiPath = generalConfig?.assetsPaths?.uiPath ?? "https://ujsa.studioplc.fr/datas/UI/";
        string url = uiPath + "fleche_formation.png";

        yield return StartCoroutine(LoadRemoteSprite(url, sprite =>
        {
            cachedFormationArrowSprite = sprite;
        }, tex =>
        {
            cachedFormationArrowTexture = tex;
        }));

        // Assigner à toutes les cibles (si encore vivantes)
        for (int i = 0; i < pendingFormationArrowTargets.Count; i++)
        {
            Image img = pendingFormationArrowTargets[i];
            if (img != null)
            {
                img.sprite = cachedFormationArrowSprite;
            }
        }
        pendingFormationArrowTargets.Clear();

        isLoadingFormationArrowSprite = false;
    }
    
    void CreatePanel()
    {
        LogVerbose("[QuestPreviewPanel] 🎮 JOUER: CreatePanel() APPELÉ");
        
        // Créer un Canvas overlay dédié pour garantir le centrage
        panelRoot = new GameObject("QuestPreviewPanel");
        
        // Ajouter un Canvas en mode Overlay (comme LoginPopup)
        Canvas overlayCanvas = panelRoot.AddComponent<Canvas>();
        overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        overlayCanvas.sortingOrder = 1000;  // Très au-dessus de tout (main canvas est à ~10, settings à ~100)
        
        // CanvasScaler identique à LoginPopup
        UnityEngine.UI.CanvasScaler scaler = panelRoot.AddComponent<UnityEngine.UI.CanvasScaler>();
        scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize;
        scaler.referenceResolution = new Vector2(1920, 1080);
        scaler.matchWidthOrHeight = 0f;
        
        // GraphicRaycaster pour détecter les clics
        UnityEngine.UI.GraphicRaycaster raycaster = panelRoot.AddComponent<UnityEngine.UI.GraphicRaycaster>();
        raycaster.blockingObjects = UnityEngine.UI.GraphicRaycaster.BlockingObjects.All;
        
        LogVerbose($"[QuestPreviewPanel] Canvas overlay créé (sortingOrder={overlayCanvas.sortingOrder}, raycaster activé)");
        
        // Fond sombre (plein écran)
        CreateDarkBackground();
        
        // Container du panel (centré)
        GameObject panelContainer = CreatePanelContainer();
        
        // Header avec titre et bouton fermer
        CreateHeader(panelContainer.transform);
        
        // Description
        CreateDescription(panelContainer.transform);
        
        // Contenu principal (vidéo + infos)
        CreateMainContent(panelContainer.transform);
        
        // Lien d'aide (désactivé)
        // CreateHelpLink(panelContainer.transform);
        
        // Boutons
        CreateButtons(panelContainer.transform);
        
        // Forcer la mise à jour du label du bouton après que tout soit créé
        // (car la difficulté peut être auto-sélectionnée avant la création du bouton)
        UpdateStartButtonState();
    }
    
    void CreateDarkBackground()
    {
        GameObject darkBg = new GameObject("DarkBackground");
        darkBg.transform.SetParent(panelRoot.transform, false);
        
        RectTransform rect = darkBg.AddComponent<RectTransform>();
        rect.anchorMin = Vector2.zero;
        rect.anchorMax = Vector2.one;
        rect.offsetMin = Vector2.zero;
        rect.offsetMax = Vector2.zero;
        
        Image image = darkBg.AddComponent<Image>();
        image.color = HexToColor(config.darkBackgroundColor);
        image.raycastTarget = true; // Bloque TOUS les clics sans action - ne ferme PAS le panel
        
        // Ajouter un EventTrigger pour capturer explicitement les clics et les empêcher de passer
        UnityEngine.EventSystems.EventTrigger eventTrigger = darkBg.AddComponent<UnityEngine.EventSystems.EventTrigger>();
        
        // Capturer les clics (PointerClick)
        UnityEngine.EventSystems.EventTrigger.Entry clickEntry = new UnityEngine.EventSystems.EventTrigger.Entry();
        clickEntry.eventID = UnityEngine.EventSystems.EventTriggerType.PointerClick;
        clickEntry.callback.AddListener((data) => {
            LogVerbose("[QuestPreviewPanel] Clic sur le fond sombre capturé et bloqué");
            // Ne rien faire - juste bloquer le clic
        });
        eventTrigger.triggers.Add(clickEntry);
        
        // Capturer aussi les PointerDown pour être encore plus sûr
        UnityEngine.EventSystems.EventTrigger.Entry downEntry = new UnityEngine.EventSystems.EventTrigger.Entry();
        downEntry.eventID = UnityEngine.EventSystems.EventTriggerType.PointerDown;
        downEntry.callback.AddListener((data) => {
            LogVerbose("[QuestPreviewPanel] PointerDown sur le fond sombre capturé et bloqué");
            // Ne rien faire - juste bloquer le clic
        });
        eventTrigger.triggers.Add(downEntry);
        
        LogVerbose("[QuestPreviewPanel] Fond sombre créé avec EventTrigger - bloque les clics, ne ferme PAS le panel");
    }
    
    GameObject CreatePanelContainer()
    {
        // Structure EXACTEMENT comme SettingsPanelBuilder
        // Créer un conteneur pour tout (ombre + panneau)
        GameObject outerContainer = new GameObject("PanelOuterContainer");
        outerContainer.transform.SetParent(panelRoot.transform, false);
        
        RectTransform outerRect = outerContainer.AddComponent<RectTransform>();
        outerRect.anchorMin = new Vector2(0.5f, 0.5f);
        outerRect.anchorMax = new Vector2(0.5f, 0.5f);
        outerRect.pivot = new Vector2(0.5f, 0.5f);
        outerRect.sizeDelta = new Vector2(config.panelSize.x, config.panelSize.y);
        outerRect.anchoredPosition = Vector2.zero;
        
        // Créer l'ombre comme un élément séparé DERRIÈRE le panneau
        if (config.shadow?.enabled ?? true)
        {
            GameObject shadowObj = new GameObject("PanelShadow");
            shadowObj.transform.SetParent(outerContainer.transform, false);
            
            RectTransform shadowRect = shadowObj.AddComponent<RectTransform>();
            shadowRect.anchorMin = Vector2.zero;
            shadowRect.anchorMax = Vector2.one;
            shadowRect.sizeDelta = Vector2.zero;
            // Décaler l'ombre selon la config
            shadowRect.anchoredPosition = new Vector2(config.shadow.offsetX, -config.shadow.offsetY);
            
            Image shadowImage = shadowObj.AddComponent<Image>();
            Color shadowColor = HexToColor(config.shadow.color);
            shadowImage.sprite = CreateRoundedColorSprite((int)config.panelSize.x, (int)config.panelSize.y, 
                config.cornerRadius, shadowColor);
            shadowImage.color = Color.white;
            shadowImage.type = Image.Type.Simple;
            shadowImage.raycastTarget = false;
            
            // L'ombre doit être en premier (derrière le panneau)
            shadowObj.transform.SetAsFirstSibling();
        }
        
        // Créer le panneau principal
        GameObject container = new GameObject("PanelContainer");
        container.transform.SetParent(outerContainer.transform, false);
        
        RectTransform rect = container.AddComponent<RectTransform>();
        
        // Remplir tout le conteneur parent (EXACTEMENT comme SettingsPanelBuilder)
        rect.anchorMin = Vector2.zero;
        rect.anchorMax = Vector2.one;
        rect.sizeDelta = Vector2.zero;
        rect.offsetMin = Vector2.zero;
        rect.offsetMax = Vector2.zero;
        rect.localScale = Vector3.one;
        rect.localRotation = Quaternion.identity;
        
        Image image = container.AddComponent<Image>();
        
        // Créer un sprite arrondi avec la couleur de fond (taille exacte comme SettingsPanelBuilder)
        image.sprite = CreateRoundedColorSprite((int)config.panelSize.x, (int)config.panelSize.y, 
            config.cornerRadius, HexToColor(config.backgroundColor));
        image.color = Color.white;
        image.type = Image.Type.Simple;
        image.raycastTarget = true;
        
        LogVerbose($"[QuestPreviewPanel] Panel créé: {config.panelSize.x}x{config.panelSize.y}, radius={config.cornerRadius}");
        
        return container;
    }
    
    void CreateHeader(Transform parent)
    {
        var headerConfig = config.header;
        
        GameObject header = new GameObject("Header");
        header.transform.SetParent(parent, false);
        
        RectTransform headerRect = header.AddComponent<RectTransform>();
        headerRect.anchorMin = new Vector2(0, 1);
        headerRect.anchorMax = new Vector2(1, 1);
        headerRect.pivot = new Vector2(0.5f, 1);
        headerRect.sizeDelta = new Vector2(0, headerConfig.height);
        headerRect.anchoredPosition = Vector2.zero;
        
        Image headerImage = header.AddComponent<Image>();
        
        // Créer un sprite arrondi uniquement en haut pour le header (EXACTEMENT comme SettingsPanelBuilder)
        headerImage.sprite = CreateRoundedTopSprite((int)config.panelSize.x, (int)headerConfig.height, 
            headerConfig.cornerRadius, HexToColor(headerConfig.backgroundColor));
        headerImage.color = Color.white;
        headerImage.type = Image.Type.Simple;
        headerImage.raycastTarget = true; // Bloquer les clics
        
        // Icône horloge à gauche du titre (même hauteur que le bouton fermer, ratio respecté)
        // N'afficher que si la quête a dda=true
        if (headerIconSprite != null && currentQuestIsDda)
        {
            var closeConfig = headerConfig.closeButton;
            float iconHeight = closeConfig.size; // Même hauteur que le bouton fermer
            
            // Calculer la largeur en respectant le ratio de l'image
            float spriteRatio = headerIconSprite.rect.width / headerIconSprite.rect.height;
            float iconWidth = iconHeight * spriteRatio;
            
            GameObject icon = new GameObject("HeaderIcon");
            icon.transform.SetParent(header.transform, false);
            
            RectTransform iconRect = icon.AddComponent<RectTransform>();
            iconRect.anchorMin = new Vector2(0, 0.5f);
            iconRect.anchorMax = new Vector2(0, 0.5f);
            iconRect.pivot = new Vector2(0, 0.5f);
            iconRect.sizeDelta = new Vector2(iconWidth, iconHeight);
            iconRect.anchoredPosition = new Vector2(headerConfig.iconMarginLeft, 0);
            
            Image iconImage = icon.AddComponent<Image>();
            iconImage.sprite = headerIconSprite;
            iconImage.preserveAspect = true;
            iconImage.type = Image.Type.Simple;
        }
        
        // Titre - CENTRÉ
        GameObject titleObj = new GameObject("Title");
        titleObj.transform.SetParent(header.transform, false);
        
        RectTransform titleRect = titleObj.AddComponent<RectTransform>();
        // Centrer le titre dans le header (en laissant de l'espace pour l'icône et le bouton fermer)
        titleRect.anchorMin = new Vector2(0, 0);
        titleRect.anchorMax = new Vector2(1, 1);
        titleRect.offsetMin = Vector2.zero;
        titleRect.offsetMax = Vector2.zero;
        
        TextMeshProUGUI titleText = titleObj.AddComponent<TextMeshProUGUI>();
        titleText.text = currentQuestTitle?.ToUpper() ?? ""; // En majuscules
        titleText.fontSize = headerConfig.title.fontSize;
        
        Color titleColor = HexToColor(headerConfig.title.textColor);
        titleText.color = titleColor;
        titleText.alignment = TextAlignmentOptions.Center; // CENTRÉ au lieu de MidlineLeft
        
        LogVerbose($"[QuestPreviewPanel] Config titre - fontSize: {headerConfig.title.fontSize}, textColor: {headerConfig.title.textColor} → RGBA({titleColor.r:F3}, {titleColor.g:F3}, {titleColor.b:F3}, {titleColor.a:F3}), fontName: {headerConfig.title.fontName}");
        
        // Style de police selon fontWeight (comme dans LoginPopup)
        string fontWeight = headerConfig.title.fontWeight ?? "normal";
        if (fontWeight.ToLower() == "bold")
            titleText.fontStyle = FontStyles.Bold;
        else
            titleText.fontStyle = FontStyles.Normal;
            
        if (titleFont != null)
        {
            titleText.font = titleFont;
            LogVerbose($"[QuestPreviewPanel] ✅ Police titre appliquée: {titleFont.name}");
        }
        else
        {
            Debug.LogWarning($"[QuestPreviewPanel] ⚠️ Police titre non chargée, utilisation de la police par défaut");
        }
        
        // Bouton fermer
        CreateCloseButton(header.transform);
    }
    
    void CreateCloseButton(Transform parent)
    {
        var closeConfig = config.header.closeButton;
        
        GameObject closeBtn = new GameObject("CloseButton");
        closeBtn.transform.SetParent(parent, false);
        
        RectTransform rect = closeBtn.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(1, 0.5f);
        rect.anchorMax = new Vector2(1, 0.5f);
        rect.pivot = new Vector2(1, 0.5f);
        rect.sizeDelta = new Vector2(closeConfig.size, closeConfig.size);
        rect.anchoredPosition = new Vector2(-closeConfig.marginRight, 0);
        
        Image image = closeBtn.AddComponent<Image>();
        if (closeButtonSprite != null)
        {
            image.sprite = closeButtonSprite;
            image.color = Color.white;
        }
        else
        {
            image.color = HexToColor("#64477f");
        }
        image.raycastTarget = true;
        
        Button button = closeBtn.AddComponent<Button>();
        button.targetGraphic = image;
        button.onClick.AddListener(ClosePanel);
        
        ColorBlock colors = button.colors;
        colors.normalColor = Color.white;
        colors.highlightedColor = new Color(0.9f, 0.9f, 0.9f, 1f);
        colors.pressedColor = new Color(0.8f, 0.8f, 0.8f, 1f);
        button.colors = colors;
    }
    
    void CreateDescription(Transform parent)
    {
        var descConfig = config.description;
        
        GameObject descObj = new GameObject("Description");
        descObj.transform.SetParent(parent, false);
        
        RectTransform rect = descObj.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 1);
        rect.anchorMax = new Vector2(1, 1);
        rect.pivot = new Vector2(0.5f, 1);
        rect.anchoredPosition = new Vector2(0, -descConfig.positionY);
        rect.sizeDelta = new Vector2(-descConfig.marginHorizontal * 2, descConfig.height);
        
        TextMeshProUGUI text = descObj.AddComponent<TextMeshProUGUI>();
        text.text = currentQuestDescription;
        text.fontSize = descConfig.fontSize;
        text.color = HexToColor(descConfig.textColor);
        text.alignment = TextAlignmentOptions.Center;
        if (textFont != null) text.font = textFont;
        
        LogVerbose($"[QuestPreviewPanel] Description créée - Position Y: {descConfig.positionY}, Hauteur: {descConfig.height}");
    }
    
    void CreateMainContent(Transform parent)
    {
        // Container sans LayoutGroup - positionnement absolu
        GameObject content = new GameObject("MainContent");
        content.transform.SetParent(parent, false);
        
        RectTransform contentRect = content.AddComponent<RectTransform>();
        contentRect.anchorMin = Vector2.zero;
        contentRect.anchorMax = Vector2.one;
        contentRect.offsetMin = Vector2.zero;
        contentRect.offsetMax = Vector2.zero;
        
        // Créer chaque élément avec son positionnement absolu
        CreateDurationDifficultyCard(content.transform);
        CreateFormationsCard(content.transform);
        CreateVideoColumn(content.transform);
    }
    
    // Supprimée - utilise maintenant le positionnement absolu
    
    void CreateDurationDifficultyCard(Transform parent)
    {
        var cardConfig = config.durationDifficultyCard;
        var cardsConfig = config.infoCards;

        // Ajustement UI: décaler un peu vers la gauche pour laisser les boutons de difficulté respirer
        const float infoCardShiftX = -40f;
        
        // Créer la carte avec positionnement absolu
        GameObject card = new GameObject("DurationDifficultyCard");
        card.transform.SetParent(parent, false);
        
        RectTransform cardRect = card.AddComponent<RectTransform>();
        cardRect.anchorMin = new Vector2(0, 1); // Ancrer en haut à gauche
        cardRect.anchorMax = new Vector2(0, 1);
        cardRect.pivot = new Vector2(0, 1);
        cardRect.anchoredPosition = new Vector2(cardConfig.x, -cardConfig.y);
        cardRect.sizeDelta = new Vector2(cardConfig.width, cardConfig.height);
        
        // Image de fond
        Image bgImage = card.AddComponent<Image>();
        bgImage.sprite = CreateRoundedColorSprite((int)cardConfig.width, (int)cardConfig.height, 
            cardConfig.cornerRadius, HexToColor(cardConfig.backgroundColor));
        bgImage.type = Image.Type.Simple;
        bgImage.raycastTarget = false;
        
        // Ombre
        Shadow shadow = card.AddComponent<Shadow>();
        shadow.effectColor = HexToColor(cardConfig.shadowColor);
        shadow.effectDistance = new Vector2(0, -cardConfig.shadowOffsetY);
        
        LogVerbose($"[QuestPreviewPanel] Carte Durée/Difficulté créée - Position: ({cardConfig.x}, {cardConfig.y}), Taille: {cardConfig.width}x{cardConfig.height}");
        
        // Positionnement absolu - pas de layout automatique
        
        // Durée - position depuis le JSON
        GameObject durationSection = new GameObject("DurationSection");
        durationSection.transform.SetParent(card.transform, false);
        
        RectTransform durationRect = durationSection.AddComponent<RectTransform>();
        durationRect.anchorMin = new Vector2(0, 1);
        durationRect.anchorMax = new Vector2(0, 1);
        durationRect.pivot = new Vector2(0, 1);
        durationRect.anchoredPosition = new Vector2(cardsConfig.duration.x + infoCardShiftX, -cardsConfig.duration.y);
        
        VerticalLayoutGroup durationLayout = durationSection.AddComponent<VerticalLayoutGroup>();
        durationLayout.spacing = cardsConfig.duration.labelMarginBottom;
        durationLayout.childAlignment = TextAnchor.UpperCenter;
        durationLayout.childControlWidth = false;
        durationLayout.childControlHeight = false;
        
        ContentSizeFitter durationFitter = durationSection.AddComponent<ContentSizeFitter>();
        durationFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
        durationFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
        
        CreateCardLabel(durationSection.transform, cardsConfig.duration.label);
        CreateCardValue(durationSection.transform, currentQuestDuration ?? cardsConfig.duration.defaultValue);
        
        // Difficulté - position depuis le JSON
        GameObject difficultySection = new GameObject("DifficultySection");
        difficultySection.transform.SetParent(card.transform, false);
        
        RectTransform difficultyRect = difficultySection.AddComponent<RectTransform>();
        difficultyRect.anchorMin = new Vector2(0, 1);
        difficultyRect.anchorMax = new Vector2(0, 1);
        difficultyRect.pivot = new Vector2(0, 1);
        difficultyRect.anchoredPosition = new Vector2(cardsConfig.difficulty.x + infoCardShiftX, -cardsConfig.difficulty.y);
        
        VerticalLayoutGroup difficultyLayout = difficultySection.AddComponent<VerticalLayoutGroup>();
        difficultyLayout.spacing = cardsConfig.difficulty.labelMarginBottom;
        difficultyLayout.childAlignment = TextAnchor.UpperCenter;
        difficultyLayout.childControlWidth = false;
        difficultyLayout.childControlHeight = false;
        
        ContentSizeFitter difficultyFitter = difficultySection.AddComponent<ContentSizeFitter>();
        difficultyFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
        difficultyFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
        
        CreateCardLabel(difficultySection.transform, cardsConfig.difficulty.label);
        CreateDifficultySelector(difficultySection.transform);
        
        LogVerbose($"[QuestPreviewPanel] Durée position: ({cardsConfig.duration.x}, {cardsConfig.duration.y}), Difficulté position: ({cardsConfig.difficulty.x}, {cardsConfig.difficulty.y})");
    }
    
    void CreateDifficultySelector(Transform parent)
    {
        var diffConfig = config.infoCards.difficulty;
        difficultyButtons.Clear();
        difficultyIsCompleted.Clear();
        selectedDifficultyIndex = -1;
        
        // Container pour les boutons
        GameObject selectorObj = new GameObject("DifficultySelector");
        selectorObj.transform.SetParent(parent, false);
        
        RectTransform selectorRect = selectorObj.AddComponent<RectTransform>();
        selectorRect.anchorMin = new Vector2(0.5f, 0.5f);
        selectorRect.anchorMax = new Vector2(0.5f, 0.5f);
        selectorRect.pivot = new Vector2(0.5f, 0.5f);
        
        // Layout horizontal pour les boutons - centré
        HorizontalLayoutGroup layout = selectorObj.AddComponent<HorizontalLayoutGroup>();
        layout.spacing = 8;
        layout.childAlignment = TextAnchor.MiddleCenter;
        layout.childControlWidth = false;
        layout.childControlHeight = false;
        layout.childForceExpandWidth = false;
        layout.childForceExpandHeight = false;
        
        // ContentSizeFitter pour que le container s'adapte à ses enfants
        ContentSizeFitter fitter = selectorObj.AddComponent<ContentSizeFitter>();
        fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
        fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
        
        // Couleurs
        Color normalBgColor = HexToColor("#f5ece5");
        Color selectedBgColor = HexToColor("#a95bfb"); // Violet
        Color completedBgColor = HexToColor("#eee6fb"); // Violet très clair (badge)
        Color normalTextColor = HexToColor("#64477f");
        Color selectedTextColor = Color.white;
        
        // Créer un bouton pour chaque option
        for (int i = 0; i < diffConfig.options.Length; i++)
        {
            string optionText = diffConfig.options[i];
            int buttonIndex = i;
            const float difficultyButtonWidth = 155f;          // un peu plus étroit
            const float difficultyButtonWidthWithBadge = 175f;  // élargi si badge dans le bouton
            const float difficultyButtonHeight = 45f;
            
            // Vérifier si cette difficulté a un completion = 100
            bool isCompleted = false;
            if (currentUserProgress != null)
            {
                QuestUserProgress progress = currentUserProgress.Find(p => p.difficulty == optionText);
                if (progress != null && progress.completion == 100)
                {
                    isCompleted = true;
                    LogVerbose($"[QuestPreviewPanel] 🏅 Difficulté '{optionText}' a un completion de 100% (badge)");
                }
            }
            difficultyIsCompleted.Add(isCompleted);
            
            GameObject buttonObj = new GameObject($"DifficultyButton_{i}");
            buttonObj.transform.SetParent(selectorObj.transform, false);
            
            bool hasBadgeIcon = isCompleted && cachedBadgeDashboardSprite != null;
            float buttonWidth = hasBadgeIcon ? difficultyButtonWidthWithBadge : difficultyButtonWidth;

            RectTransform buttonRect = buttonObj.AddComponent<RectTransform>();
            buttonRect.sizeDelta = new Vector2(buttonWidth, difficultyButtonHeight);
            
            // Image de fond
            Image buttonBg = buttonObj.AddComponent<Image>();
            buttonBg.sprite = CreateRoundedColorSprite((int)buttonWidth, (int)difficultyButtonHeight, 22, Color.white);
            buttonBg.color = isCompleted ? completedBgColor : normalBgColor;
            buttonBg.type = Image.Type.Simple;
            buttonBg.raycastTarget = true;
            
            // LayoutElement pour le dimensionnement
            LayoutElement layoutElement = buttonObj.AddComponent<LayoutElement>();
            layoutElement.preferredWidth = buttonWidth;
            layoutElement.preferredHeight = difficultyButtonHeight;
            
            // Bouton
            Button button = buttonObj.AddComponent<Button>();
            button.targetGraphic = buttonBg;
            button.transition = Selectable.Transition.None; // On gère manuellement
            button.interactable = true; // ✅ On autorise le clic même si completion = 100 (badge)
            
            // Texte
            // Contenu (badge_dashboard.png + label) - on remplace la pastille par l'icône + texte
            GameObject contentObj = new GameObject("Content");
            contentObj.transform.SetParent(buttonObj.transform, false);

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

            HorizontalLayoutGroup contentLayout = contentObj.AddComponent<HorizontalLayoutGroup>();
            contentLayout.spacing = 6;
            contentLayout.childAlignment = TextAnchor.MiddleCenter;
            // Important: laisser le layout piloter les tailles via les preferred sizes (évite le wrap "vertical" du texte)
            contentLayout.childControlWidth = true;
            contentLayout.childControlHeight = true;
            contentLayout.childForceExpandWidth = false;
            contentLayout.childForceExpandHeight = false;

            // Icône uniquement si completion=100 et sprite dispo
            if (hasBadgeIcon)
            {
                GameObject badgeIconObj = new GameObject("BadgeIcon");
                badgeIconObj.transform.SetParent(contentObj.transform, false);

                RectTransform badgeIconRect = badgeIconObj.AddComponent<RectTransform>();
                badgeIconRect.sizeDelta = new Vector2(36, 36); // 2x

                Image badgeIconImg = badgeIconObj.AddComponent<Image>();
                badgeIconImg.sprite = cachedBadgeDashboardSprite;
                badgeIconImg.preserveAspect = true;
                badgeIconImg.color = Color.white;
                badgeIconImg.raycastTarget = false;

                // Donne une taille "preferred" au layout (Image ne fournit pas ILayoutElement)
                LayoutElement badgeIconLayout = badgeIconObj.AddComponent<LayoutElement>();
                badgeIconLayout.preferredWidth = 36;
                badgeIconLayout.preferredHeight = 36;
                badgeIconLayout.flexibleWidth = 0;
            }

            GameObject textObj = new GameObject("Text");
            textObj.transform.SetParent(contentObj.transform, false);

            // RectTransform requis pour les composants UI
            RectTransform textRect = textObj.AddComponent<RectTransform>();
            textRect.sizeDelta = Vector2.zero;

            TextMeshProUGUI buttonText = textObj.AddComponent<TextMeshProUGUI>();
            buttonText.text = optionText;
            buttonText.fontSize = diffConfig.buttonFontSize;
            buttonText.color = normalTextColor;
            buttonText.alignment = hasBadgeIcon ? TextAlignmentOptions.MidlineLeft : TextAlignmentOptions.Center;
            buttonText.fontStyle = FontStyles.Normal;
            buttonText.enableWordWrapping = false;
            buttonText.overflowMode = TextOverflowModes.Ellipsis;
            if (textFont != null) buttonText.font = textFont;

            // Laisser le texte prendre l'espace restant sans forcer un wrap
            LayoutElement textLayout = textObj.AddComponent<LayoutElement>();
            textLayout.minWidth = 0;
            textLayout.flexibleWidth = 1;
            
            // ✅ Toujours cliquable (même en completion 100)
                button.onClick.AddListener(() => {
                    SelectDifficulty(buttonIndex);
                });
            
            difficultyButtons.Add(button);
        }
        
        // Déterminer quelle difficulté sélectionner automatiquement
        int autoSelectIndex = DetermineAutoSelectDifficulty(diffConfig.options);
        
        if (autoSelectIndex >= 0)
        {
            SelectDifficulty(autoSelectIndex);
            LogVerbose($"[QuestPreviewPanel] 📊 Difficulté auto-sélectionnée: {diffConfig.options[autoSelectIndex]} (index: {autoSelectIndex})");
        }
        else
        {
            LogVerbose($"[QuestPreviewPanel] 📊 Aucune difficulté sélectionnée (toutes les completions sont à 0 ou pas de données)");
        }
        
        LogVerbose($"[QuestPreviewPanel] Sélecteur de difficulté créé avec {diffConfig.options.Length} boutons");
        
        // Mettre à jour l'état du bouton démarrer
        UpdateStartButtonState();
    }
    
    /// <summary>
    /// Détermine quelle difficulté sélectionner automatiquement basé sur la progression utilisateur
    /// </summary>
    /// <returns>Index de la difficulté à sélectionner, ou -1 si aucune</returns>
    private int DetermineAutoSelectDifficulty(string[] difficultyOptions)
    {
        // Réinitialiser l'index de difficulté en cours
        inProgressDifficultyIndex = -1;
        
        // Si pas de données de progression utilisateur, ne rien sélectionner
        if (currentUserProgress == null || currentUserProgress.Count == 0)
        {
            LogVerbose($"[QuestPreviewPanel] Pas de données de progression utilisateur");
            return -1;
        }
        
        // Vérifier si toutes les completions sont à 0
        bool allZero = true;
        int inProgressIndex = -1;
        
        foreach (var progress in currentUserProgress)
        {
            if (progress.completion > 0)
            {
                allZero = false;
                
                // Si completion > 0 et < 100, c'est la difficulté en cours
                if (progress.completion < 100)
                {
                    // Trouver l'index correspondant dans les options
                    for (int i = 0; i < difficultyOptions.Length; i++)
                    {
                        if (difficultyOptions[i] == progress.difficulty)
                        {
                            inProgressIndex = i;
                            inProgressDifficultyIndex = i; // Stocker dans la variable de classe
                            LogVerbose($"[QuestPreviewPanel] Difficulté en cours trouvée: {progress.difficulty} (completion={progress.completion}%, index={i})");
                            break;
                        }
                    }
                }
            }
        }
        
        // Si toutes les completions sont à 0, ne rien sélectionner
        if (allZero)
        {
            LogVerbose($"[QuestPreviewPanel] Toutes les completions sont à 0 - aucune sélection");
            return -1;
        }
        
        // Si une difficulté est en cours (completion > 0 et < 100), la sélectionner
        if (inProgressIndex >= 0)
        {
            return inProgressIndex;
        }
        
        // Sinon (toutes les difficultés commencées sont à 100%), 
        // vérifier la difficulté sauvegardée localement, mais seulement si elle n'est pas à 100%
        string savedDifficulty = UserDataManager.Instance?.GetQuestDifficulty(currentQuestId, null);
        if (!string.IsNullOrEmpty(savedDifficulty))
        {
            int savedIndex = System.Array.IndexOf(difficultyOptions, savedDifficulty);
            if (savedIndex >= 0)
            {
                // Vérifier si cette difficulté sauvegardée n'est pas à 100%
                QuestUserProgress savedProgress = currentUserProgress?.Find(p => p.difficulty == savedDifficulty);
                if (savedProgress == null || savedProgress.completion < 100)
                {
                    LogVerbose($"[QuestPreviewPanel] Utilisation de la difficulté sauvegardée: {savedDifficulty}");
                    return savedIndex;
                }
                else
                {
                    LogVerbose($"[QuestPreviewPanel] Difficulté sauvegardée '{savedDifficulty}' est à 100% - non sélectionnée");
                }
            }
        }
        
        return -1;
    }
    
    void SelectDifficulty(int index)
    {
        var diffConfig = config.infoCards.difficulty;
        
        // Couleurs
        Color normalBgColor = HexToColor("#f5ece5");
        Color selectedBgColor = HexToColor("#a95bfb"); // Violet
        Color completedBgColor = HexToColor("#eee6fb"); // Violet très clair (badge)
        Color normalTextColor = HexToColor("#64477f");
        Color selectedTextColor = Color.white;
        
        // Mettre à jour l'apparence de tous les boutons
        for (int i = 0; i < difficultyButtons.Count; i++)
        {
            Button btn = difficultyButtons[i];
            Image bgImage = btn.GetComponent<Image>();
            TextMeshProUGUI btnText = btn.GetComponentInChildren<TextMeshProUGUI>();
            
            if (i == index)
            {
                // Sélectionné
                bgImage.color = selectedBgColor;
                if (btnText != null) btnText.color = selectedTextColor;
            }
            else
            {
                // Non sélectionné
                bool completed = (i >= 0 && i < difficultyIsCompleted.Count) ? difficultyIsCompleted[i] : false;
                bgImage.color = completed ? completedBgColor : normalBgColor;
                if (btnText != null) btnText.color = normalTextColor;
            }
        }
        
        selectedDifficultyIndex = index;
        
        // Sauvegarder la sélection
        if (index >= 0 && index < diffConfig.options.Length)
        {
            string difficulty = diffConfig.options[index];
            UserDataManager.Instance.SetQuestDifficulty(currentQuestId, difficulty);
            LogVerbose($"[QuestPreviewPanel] 💾 Difficulté '{difficulty}' sélectionnée et sauvegardée");
        }
        
        // Mettre à jour l'état du bouton démarrer
        UpdateStartButtonState();
    }
    
    void CreateFormationsCard(Transform parent)
    {
        var cardConfig = config.formationsCard;
        var cardsConfig = config.infoCards;
        var formationsConfig = cardsConfig.formations;
        
        // Créer la carte avec positionnement absolu
        GameObject card = new GameObject("FormationsCard");
        card.transform.SetParent(parent, false);
        
        RectTransform cardRect = card.AddComponent<RectTransform>();
        cardRect.anchorMin = new Vector2(0, 1); // Ancrer en haut à gauche
        cardRect.anchorMax = new Vector2(0, 1);
        cardRect.pivot = new Vector2(0, 1);
        cardRect.anchoredPosition = new Vector2(cardConfig.x, -cardConfig.y);
        cardRect.sizeDelta = new Vector2(cardConfig.width, cardConfig.height);
        
        // Image de fond
        Image bgImage = card.AddComponent<Image>();
        bgImage.sprite = CreateRoundedColorSprite((int)cardConfig.width, (int)cardConfig.height, 
            cardConfig.cornerRadius, HexToColor(cardConfig.backgroundColor));
        bgImage.type = Image.Type.Simple;
        bgImage.raycastTarget = false;
        
        // Ombre
        Shadow shadow = card.AddComponent<Shadow>();
        shadow.effectColor = HexToColor(cardConfig.shadowColor);
        shadow.effectDistance = new Vector2(0, -cardConfig.shadowOffsetY);
        
        LogVerbose($"[QuestPreviewPanel] Carte Formations créée - Position: ({cardConfig.x}, {cardConfig.y}), Taille: {cardConfig.width}x{cardConfig.height}");
        
        // Pas de VerticalLayoutGroup sur la carte - positionnement manuel
        
        // Titre avec marge configurable
        GameObject labelObj = new GameObject("Label");
        labelObj.transform.SetParent(card.transform, false);
        
        RectTransform labelRect = labelObj.AddComponent<RectTransform>();
        labelRect.anchorMin = new Vector2(0, 1);
        labelRect.anchorMax = new Vector2(1, 1);
        labelRect.pivot = new Vector2(0.5f, 1);
        labelRect.anchoredPosition = new Vector2(0, -cardConfig.padding);
        labelRect.sizeDelta = new Vector2(-cardConfig.padding * 2, 30);
        
        TextMeshProUGUI labelText = labelObj.AddComponent<TextMeshProUGUI>();
        labelText.text = formationsConfig.label;
        labelText.fontSize = config.infoCards.cardTitle.fontSize;
        labelText.color = HexToColor(config.infoCards.cardTitle.textColor);
        labelText.alignment = TextAlignmentOptions.Center;
        labelText.fontStyle = FontStyles.Normal;
        if (titleFont != null) labelText.font = titleFont;
        
        // Container pour les items de formation
        GameObject itemsContainer = new GameObject("ItemsContainer");
        itemsContainer.transform.SetParent(card.transform, false);
        
        RectTransform containerRect = itemsContainer.AddComponent<RectTransform>();
        containerRect.anchorMin = new Vector2(0, 1);
        containerRect.anchorMax = new Vector2(1, 1);
        containerRect.pivot = new Vector2(0.5f, 1);
        float topOffset = cardConfig.padding + 30 + formationsConfig.labelMarginBottom; // padding + hauteur label + marge
        containerRect.anchoredPosition = new Vector2(0, -topOffset);
        containerRect.sizeDelta = new Vector2(-cardConfig.padding * 2, cardConfig.height - topOffset - cardConfig.padding);
        
        VerticalLayoutGroup layout = itemsContainer.AddComponent<VerticalLayoutGroup>();
        layout.spacing = formationsConfig.itemSpacing;
        layout.padding = new RectOffset(0, 0, 0, 0);
        layout.childAlignment = TextAnchor.UpperLeft;
        layout.childControlWidth = true;
        layout.childControlHeight = false;
        layout.childForceExpandWidth = true;
        layout.childForceExpandHeight = false;
        
        LogVerbose($"[QuestPreviewPanel] Formations: labelMarginBottom={formationsConfig.labelMarginBottom}px, itemSpacing={formationsConfig.itemSpacing}px");
        
        // Liste des formations (utiliser currentTrainings si disponible, sinon fallback)
        if (currentTrainings != null && currentTrainings.Count > 0)
        {
            foreach (Training training in currentTrainings)
            {
                CreateFormationItem(itemsContainer.transform, training.title, training.url);
            }
        }
        else if (currentFormations != null && currentFormations.Length > 0)
        {
            foreach (string formation in currentFormations)
            {
                CreateFormationItem(itemsContainer.transform, formation, "");
            }
        }
        else if (formationsConfig.items != null)
        {
            foreach (string formation in formationsConfig.items)
            {
                CreateFormationItem(itemsContainer.transform, formation, "");
            }
        }
    }
    
    void CreateFormationItem(Transform parent, string text, string url)
    {
        var formationsConfig = config.infoCards.formations;
        
        GameObject item = new GameObject("FormationItem");
        item.transform.SetParent(parent, false);
        
        RectTransform itemRect = item.AddComponent<RectTransform>();
        itemRect.sizeDelta = new Vector2(0, formationsConfig.itemHeight);
        
        // Ajouter LayoutElement avec hauteur fixe
        LayoutElement itemLayoutElement = item.AddComponent<LayoutElement>();
        itemLayoutElement.preferredHeight = formationsConfig.itemHeight;
        itemLayoutElement.minHeight = formationsConfig.itemHeight;
        itemLayoutElement.flexibleHeight = 0;
        
        HorizontalLayoutGroup layout = item.AddComponent<HorizontalLayoutGroup>();
        layout.spacing = 5;
        layout.padding = new RectOffset(0, 0, 0, 0);
        layout.childAlignment = TextAnchor.MiddleLeft;
        layout.childControlWidth = true;
        layout.childControlHeight = true;
        layout.childForceExpandWidth = false;
        
        // Bullet/Arrow - Image PNG (40x40)
        GameObject bullet = new GameObject("Bullet");
        bullet.transform.SetParent(item.transform, false);
        
        RectTransform bulletRect = bullet.AddComponent<RectTransform>();
        bulletRect.sizeDelta = new Vector2(40, 40);
        
        // Ajouter LayoutElement pour fixer la taille de la puce (FIXE 40x40)
        LayoutElement bulletLayout = bullet.AddComponent<LayoutElement>();
        bulletLayout.minWidth = 40;
        bulletLayout.preferredWidth = 40;
        bulletLayout.minHeight = 40;
        bulletLayout.preferredHeight = 40;
        bulletLayout.flexibleWidth = 0;   // Ne pas étirer horizontalement
        bulletLayout.flexibleHeight = 0;  // Ne pas étirer verticalement
        
        Image bulletImage = bullet.AddComponent<Image>();
        bulletImage.preserveAspect = true;
        
        // Charger l'image de la flèche
        StartCoroutine(LoadFormationArrowSprite(bulletImage));
        
        // Texte (cliquable si URL fournie) - prend toute la largeur restante
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(item.transform, false);
        
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        
        // Ajouter LayoutElement pour que le texte prenne toute la largeur disponible
        LayoutElement textLayout = textObj.AddComponent<LayoutElement>();
        textLayout.flexibleWidth = 1;  // Prendre toute la largeur disponible
        // Pas de hauteur fixe - s'adapte au contenu
        
        TextMeshProUGUI textComp = textObj.AddComponent<TextMeshProUGUI>();
        textComp.text = text;
        
        // Utiliser la config des formations pour le style
        textComp.fontSize = formationsConfig.fontSize;
        
        // Couleur et style selon si c'est un lien ou non
        bool isLink = !string.IsNullOrEmpty(url);
        textComp.color = HexToColor(isLink ? formationsConfig.linkColor : formationsConfig.textColor);
        
        textComp.alignment = TextAlignmentOptions.TopLeft;  // Alignement en haut à gauche
        textComp.textWrappingMode = TextWrappingModes.Normal;  // Activer le retour à la ligne
        textComp.overflowMode = TextOverflowModes.Overflow;  // Permettre au texte de s'étendre verticalement
        textComp.margin = new Vector4(0, 0, 0, 0);  // Pas de marge
        textComp.enableAutoSizing = false;  // Pas de redimensionnement automatique
        
        // Souligner si c'est un lien et que l'option est activée
        if (isLink && formationsConfig.underlineLinks)
        {
            textComp.fontStyle = FontStyles.Underline;
        }
        
        if (textFont != null) textComp.font = textFont;
        
        // Rendre cliquable si URL fournie
        if (isLink)
        {
            // Ajouter un bouton pour rendre le texte cliquable
            Button button = textObj.AddComponent<Button>();
            button.transition = Selectable.Transition.ColorTint;
            
            ColorBlock colors = button.colors;
            colors.normalColor = Color.white;
            colors.highlightedColor = new Color(0.95f, 0.9f, 1f, 1f); // Légèrement teinté au survol
            colors.pressedColor = new Color(0.85f, 0.75f, 0.95f, 1f);
            button.colors = colors;
            
            button.onClick.AddListener(() => {
                LogVerbose($"[QuestPreviewPanel] Ouverture de la formation: {text} → {url}");
                Application.OpenURL(url);
            });
            
            // Changer le style du texte pour indiquer qu'il est cliquable
            textComp.fontStyle = FontStyles.Underline;
        }
    }
    
    // OBSOLÈTE - Utilise maintenant le positionnement absolu dans CreateDurationDifficultyCard et CreateFormationsCard
    
    void CreateCardLabel(Transform parent, string text)
    {
        GameObject labelObj = new GameObject("Label");
        labelObj.transform.SetParent(parent, false);
        
        RectTransform rect = labelObj.AddComponent<RectTransform>();
        rect.sizeDelta = new Vector2(320, 30);  // Augmenté pour tenir "NIVEAU DE DIFFICULTÉ" sur une ligne
        
        // Ajouter LayoutElement pour contrôler la hauteur exacte
        LayoutElement layoutElement = labelObj.AddComponent<LayoutElement>();
        layoutElement.preferredHeight = 30;  // Hauteur exacte sans expansion
        layoutElement.minHeight = 30;
        
        TextMeshProUGUI labelText = labelObj.AddComponent<TextMeshProUGUI>();
        labelText.text = text;
        labelText.fontSize = config.infoCards.cardTitle.fontSize;
        labelText.color = HexToColor(config.infoCards.cardTitle.textColor);
        labelText.alignment = TextAlignmentOptions.Center;
        labelText.fontStyle = FontStyles.Normal;
        labelText.textWrappingMode = TextWrappingModes.NoWrap;  // Pas de retour à la ligne
        labelText.overflowMode = TextOverflowModes.Overflow;  // Permettre le débordement si nécessaire
        labelText.margin = new Vector4(0, 0, 0, 0);  // Pas de marge
        if (titleFont != null) labelText.font = titleFont;
    }
    
    void CreateCardValue(Transform parent, string text)
    {
        GameObject valueObj = new GameObject("Value");
        valueObj.transform.SetParent(parent, false);
        
        RectTransform rect = valueObj.AddComponent<RectTransform>();
        rect.sizeDelta = new Vector2(200, 30);
        
        TextMeshProUGUI valueText = valueObj.AddComponent<TextMeshProUGUI>();
        valueText.text = text;
        valueText.fontSize = config.infoCards.cardText.fontSize + 4;
        valueText.color = HexToColor(config.infoCards.cardText.textColor);
        valueText.alignment = TextAlignmentOptions.Center;
        if (textFont != null) valueText.font = textFont;
    }
    
    void CreateVideoColumn(Transform parent)
    {
        var videoConfig = config.videoPlayer;
        
        GameObject column = new GameObject("VideoColumn");
        column.transform.SetParent(parent, false);
        
        // Positionnement absolu
        RectTransform columnRect = column.AddComponent<RectTransform>();
        columnRect.anchorMin = new Vector2(0, 1); // Ancrer en haut à gauche
        columnRect.anchorMax = new Vector2(0, 1);
        columnRect.pivot = new Vector2(0, 1);
        columnRect.anchoredPosition = new Vector2(videoConfig.x, -videoConfig.y);
        columnRect.sizeDelta = new Vector2(videoConfig.width, videoConfig.height);
        
        LogVerbose($"[QuestPreviewPanel] Vidéo créée - Position: ({videoConfig.x}, {videoConfig.y}), Taille: {videoConfig.width}x{videoConfig.height}, Radius: {videoConfig.cornerRadius}");
        
        Image bgImage = column.AddComponent<Image>();
        bgImage.sprite = CreateRoundedColorSprite((int)videoConfig.width, (int)videoConfig.height, 
            videoConfig.cornerRadius, new Color(0.2f, 0.2f, 0.3f));
        bgImage.type = Image.Type.Simple;
        bgImage.raycastTarget = true; // Bloquer les clics
        
        // Masque pour les coins arrondis de la vidéo
        Mask mask = column.AddComponent<Mask>();
        mask.showMaskGraphic = true;
        
        // En WebGL, VideoPlayer + RenderTexture est très coûteux et peut mener à OOM.
        // On conserve l'UI mais on évite la lecture intégrée.
        #if UNITY_WEBGL && !UNITY_EDITOR
        CreateWebglVideoPlaceholder(column.transform, videoConfig.width, videoConfig.height);
        return;
        #endif

        // Video Player - utiliser la vidéo par défaut si aucune vidéo n'est fournie
        string videoUrl = !string.IsNullOrEmpty(currentVideoUrl) 
            ? currentVideoUrl 
            : "https://ujsa.studioplc.fr/datas/videos/Q00_intro.mp4";
        
        LogVerbose($"[QuestPreviewPanel] 🎬 URL vidéo: {videoUrl} (défaut: {string.IsNullOrEmpty(currentVideoUrl)})");
        
        currentVideoPlayer = column.AddComponent<VideoPlayer>();
        currentVideoPlayer.source = VideoSource.Url;
        currentVideoPlayer.url = videoUrl;
        currentVideoPlayer.isLooping = true;
        currentVideoPlayer.playOnAwake = false; // On va démarrer manuellement après la préparation
        currentVideoPlayer.renderMode = VideoRenderMode.RenderTexture;
        currentVideoPlayer.audioOutputMode = VideoAudioOutputMode.None; // Désactiver le son
        currentVideoPlayer.skipOnDrop = true;
        currentVideoPlayer.waitForFirstFrame = true;
        
        // RawImage pour afficher la vidéo
        GameObject videoDisplay = new GameObject("VideoDisplay");
        videoDisplay.transform.SetParent(column.transform, false);
        
        RectTransform videoRect = videoDisplay.AddComponent<RectTransform>();
        videoRect.anchorMin = Vector2.zero;
        videoRect.anchorMax = Vector2.one;
        videoRect.offsetMin = Vector2.zero;
        videoRect.offsetMax = Vector2.zero;
        
        currentVideoRawImage = videoDisplay.AddComponent<RawImage>();
        currentVideoRawImage.color = Color.white;
        
        // Créer une RenderTexture pour la vidéo
        // Réduction légère (on évite 1280x720 systématique)
        currentVideoRenderTexture = new RenderTexture(960, 540, 0);
        currentVideoRenderTexture.Create();
        dynamicRenderTextures.Add(currentVideoRenderTexture);
        currentVideoPlayer.targetTexture = currentVideoRenderTexture;
        currentVideoRawImage.texture = currentVideoRenderTexture;
        
        LogVerbose($"[QuestPreviewPanel] 🎬 VideoPlayer configuré - RenderTexture: {currentVideoRenderTexture.width}x{currentVideoRenderTexture.height}");
        
        // Gestion des événements
        currentVideoPlayer.prepareCompleted += OnVideoPrepareCompleted;
        currentVideoPlayer.started += OnVideoStarted;
        currentVideoPlayer.errorReceived += OnVideoErrorReceived;
        currentVideoPlayer.loopPointReached += OnVideoLoopPointReached;
        
        // Démarrer la préparation
        LogVerbose($"[QuestPreviewPanel] 🎬 Démarrage de la préparation de la vidéo...");
        currentVideoPlayer.Prepare();
    }

    private void OnVideoPrepareCompleted(VideoPlayer source)
    {
        LogVerbose("[QuestPreviewPanel] ✅ Vidéo préparée, démarrage de la lecture...");
        if (source != null) source.Play();
    }

    private void OnVideoStarted(VideoPlayer source)
    {
        LogVerbose("[QuestPreviewPanel] ▶️ Vidéo démarrée!");
    }

    private void OnVideoErrorReceived(VideoPlayer source, string message)
    {
        Debug.LogError($"[QuestPreviewPanel] ❌ Erreur vidéo: {message}");
    }

    private void OnVideoLoopPointReached(VideoPlayer source)
    {
        LogVerbose("[QuestPreviewPanel] 🔄 Boucle vidéo atteinte");
    }

    private void CreateWebglVideoPlaceholder(Transform parent, float width, float height)
    {
        // Placeholder simple: texte + bouton ouvrant la vidéo dans un onglet (évite OOM WebGL)
        GameObject placeholder = new GameObject("VideoPlaceholder");
        placeholder.transform.SetParent(parent, false);

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

        Image bg = placeholder.AddComponent<Image>();
        bg.color = new Color(0f, 0f, 0f, 0.25f);
        bg.raycastTarget = true;

        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(placeholder.transform, false);
        RectTransform tr = textObj.AddComponent<RectTransform>();
        tr.anchorMin = Vector2.zero;
        tr.anchorMax = Vector2.one;
        tr.offsetMin = new Vector2(20, 20);
        tr.offsetMax = new Vector2(-20, -20);

        TextMeshProUGUI t = textObj.AddComponent<TextMeshProUGUI>();
        t.text = "Aperçu vidéo indisponible en WebGL.\nCliquez pour ouvrir la vidéo.";
        t.alignment = TextAlignmentOptions.Center;
        t.fontSize = 26;
        t.color = Color.white;
        if (textFont != null) t.font = textFont;

        Button btn = placeholder.AddComponent<Button>();
        btn.transition = Selectable.Transition.ColorTint;
        btn.onClick.AddListener(() =>
        {
            string url = !string.IsNullOrEmpty(currentVideoUrl)
                ? currentVideoUrl
                : "https://ujsa.studioplc.fr/datas/videos/Q00_intro.mp4";
            Application.OpenURL(url);
        });
    }
    
    void CreateHelpLink(Transform parent)
    {
        var helpConfig = config.helpLink;
        
        GameObject helpObj = new GameObject("HelpLink");
        helpObj.transform.SetParent(parent, false);
        
        // Positionnement absolu
        RectTransform rect = helpObj.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 1); // Ancrer en haut à gauche
        rect.anchorMax = new Vector2(0, 1);
        rect.pivot = new Vector2(0.5f, 0.5f); // Pivot au centre pour centrer le contenu
        rect.anchoredPosition = new Vector2(helpConfig.x + helpConfig.width / 2, -helpConfig.y - helpConfig.height / 2);
        rect.sizeDelta = new Vector2(helpConfig.width, helpConfig.height);
        
        LogVerbose($"[QuestPreviewPanel] Lien d'aide créé - Position: ({helpConfig.x}, {helpConfig.y}), Taille: {helpConfig.width}x{helpConfig.height}");
        
        HorizontalLayoutGroup layout = helpObj.AddComponent<HorizontalLayoutGroup>();
        layout.spacing = 8;
        layout.childAlignment = TextAnchor.MiddleCenter;
        layout.childControlWidth = false;
        layout.childControlHeight = false;
        
        // Icône ?
        GameObject iconObj = new GameObject("Icon");
        iconObj.transform.SetParent(helpObj.transform, false);
        
        RectTransform iconRect = iconObj.AddComponent<RectTransform>();
        iconRect.sizeDelta = new Vector2(helpConfig.iconSize, helpConfig.iconSize);
        
        TextMeshProUGUI iconText = iconObj.AddComponent<TextMeshProUGUI>();
        iconText.text = "?";
        iconText.fontSize = helpConfig.fontSize;
        iconText.color = HexToColor(helpConfig.textColor);
        iconText.alignment = TextAlignmentOptions.Center;
        iconText.fontStyle = FontStyles.Bold;
        
        // Cercle autour du ?
        Outline iconOutline = iconObj.AddComponent<Outline>();
        iconOutline.effectColor = HexToColor(helpConfig.textColor);
        iconOutline.effectDistance = new Vector2(1, -1);
        
        // Texte
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(helpObj.transform, false);
        
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.sizeDelta = new Vector2(150, 25);
        
        TextMeshProUGUI text = textObj.AddComponent<TextMeshProUGUI>();
        text.text = helpConfig.text;
        text.fontSize = helpConfig.fontSize;
        text.color = HexToColor(helpConfig.textColor);
        text.alignment = TextAlignmentOptions.MidlineLeft;
        text.fontStyle = FontStyles.Bold;
        
        // Bouton
        Button button = helpObj.AddComponent<Button>();
        button.transition = Selectable.Transition.ColorTint;
        button.onClick.AddListener(() => {
            LogVerbose("[QuestPreviewPanel] Aide demandée");
            // TODO: Ouvrir l'aide
        });
    }
    
    void CreateButtons(Transform parent)
    {
        var buttonsConfig = config.buttons;
        
        // Pas de container - positionnement direct des boutons
        
        // Bouton "Passer cette quête" - uniquement si la quête suivante n'est pas déjà disponible
        if (!isNextQuestAvailable)
        {
        CreateActionButton(parent, buttonsConfig.skipQuest, false, () => {
            StartCoroutine(SkipQuestAndReload());
        });
            LogVerbose("[QuestPreviewPanel] ✅ Bouton 'Passer cette quête' affiché");
        }
        else
        {
            LogVerbose("[QuestPreviewPanel] ⏭️ Bouton 'Passer cette quête' masqué - la quête suivante est déjà disponible");
        }
        
        // Texte d'avertissement juste au-dessus du bouton "Commencer la quête"
        CreateWarningText(parent, buttonsConfig.startQuest);
        
        // Bouton "Commencer la quête"
        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        var enabledStyle = generalConfig?.buttonStyles?.GetStyle(buttonsConfig.startQuest.style);
        var disabledStyle = generalConfig?.buttonStyles?.GetStyle(buttonsConfig.startQuest.disabledStyle);
        
        // Créer les sprites avec les dimensions du bouton
        if (enabledStyle != null)
        {
            Color startColor = HexToColor(enabledStyle.gradient?.startColor ?? "#b87aff");
            Color endColor = HexToColor(enabledStyle.gradient?.endColor ?? "#7b4fbf");
            Color borderColor = HexToColor(enabledStyle.borderColor ?? "#f5ece5");
            startButtonEnabledSprite = CreateGradientSpriteWithBorder((int)buttonsConfig.startQuest.width, (int)buttonsConfig.startQuest.height, 
                enabledStyle.borderRadius, startColor, endColor, borderColor, enabledStyle.borderWidth);
        }
        
        if (disabledStyle != null)
        {
            Color startColor = HexToColor(disabledStyle.gradient?.startColor ?? "#cccccc");
            Color endColor = HexToColor(disabledStyle.gradient?.endColor ?? "#999999");
            Color borderColor = HexToColor(disabledStyle.borderColor ?? "#f5ece5");
            startButtonDisabledSprite = CreateGradientSpriteWithBorder((int)buttonsConfig.startQuest.width, (int)buttonsConfig.startQuest.height, 
                disabledStyle.borderRadius, startColor, endColor, borderColor, disabledStyle.borderWidth);
        }
        
        startButton = CreateActionButton(parent, buttonsConfig.startQuest, true, () => {
            if (selectedDifficultyIndex >= 0) // Un niveau a été sélectionné
            {
                // Cas "RECOMMENCER" : completion = 100 sur la difficulté sélectionnée
                var diffConfig = config.infoCards.difficulty;
                if (diffConfig != null && diffConfig.options != null && selectedDifficultyIndex < diffConfig.options.Length)
                {
                    string selectedDifficulty = diffConfig.options[selectedDifficultyIndex];
                    QuestUserProgress selectedProgress = currentUserProgress?.Find(p => p.difficulty == selectedDifficulty);
                    if (selectedProgress != null && selectedProgress.completion == 100)
                    {
                        string msg =
                            $"En recommencant la quete \"{currentQuestTitle}\" avec le niveau {selectedDifficulty}, vous allez annuler votre badge.";
                        ShowConfirmationModalWithCallback(msg, () =>
                        {
                            StartCoroutine(ResetBadgeByDifficultyAndStartQuest(selectedDifficulty));
                        });
                        return;
                    }
                }

                // 1. Vérifier d'abord si une AUTRE quête a une partie en cours
                var otherQuestInProgress = FindOtherQuestInProgress();
                if (otherQuestInProgress != null)
                {
                    // Afficher la modale de confirmation pour l'autre quête
                    ShowConfirmationModalForOtherQuest(otherQuestInProgress);
                    return;
                }
                
                // 2. Vérifier si l'utilisateur change de difficulté dans la MÊME quête
                if (inProgressDifficultyIndex >= 0 && selectedDifficultyIndex != inProgressDifficultyIndex)
                {
                    // Afficher la modale de confirmation pour la même quête
                    var diffConfig2 = config.infoCards.difficulty;
                    if (diffConfig2 != null && diffConfig2.options != null && inProgressDifficultyIndex < diffConfig2.options.Length)
                    {
                        string inProgressDifficulty = diffConfig2.options[inProgressDifficultyIndex];
                        ShowConfirmationModal($"Vous avez déjà une partie en cours dans la quête \"{currentQuestTitle}\" niveau de difficulté {inProgressDifficulty}. Elle va être effacée.");
                    }
                    return;
                }
                
                // 3. Pas de conflit, lancer directement
                SaveLastCompletedGameForSelectedDifficulty();
                    ClosePanel();
                    onStartQuest?.Invoke();
            }
        });
        
        startButtonImage = startButton.GetComponent<Image>();
        startButtonText = startButton.GetComponentInChildren<TextMeshProUGUI>();
        
        // Forcer la mise à jour du texte d'avertissement après création
        UpdateStartButtonState();
        
        // Vérifier que le texte d'avertissement est bien créé
        if (warningText != null)
        {
            LogVerbose($"[QuestPreviewPanel] ✅ WarningText créé et prêt - Active: {warningText.gameObject.activeSelf}");
        }
        else
        {
            Debug.LogError("[QuestPreviewPanel] ❌ WarningText est null après création !");
        }
    }
    
    void CreateWarningText(Transform parent, QuestPreviewButtonConfig buttonConfig)
    {
        var buttonsConfig = config.buttons;
        
        GameObject warningObj = new GameObject("WarningText");
        warningObj.transform.SetParent(parent, false);
        
        RectTransform warningRect = warningObj.AddComponent<RectTransform>();
        // Utiliser les mêmes ancres que le bouton pour être sûr d'être dans le même système de coordonnées
        warningRect.anchorMin = new Vector2(0, 1);
        warningRect.anchorMax = new Vector2(0, 1);
        warningRect.pivot = new Vector2(0, 1);
        // Positionner juste au-dessus du bouton
        // Le bouton est à anchoredPosition.y = -buttonsConfig.y (Y négatif = vers le bas depuis le haut)
        // Pour être au-dessus: y doit être moins négatif (plus proche de 0)
        // Donc: y = -(buttonsConfig.y - warningHeight - spacing)
        float warningHeight = 50f;
        float spacing = 30f;
        float warningY = buttonsConfig.y - warningHeight - spacing; // Moins grand = plus haut visuellement
        warningRect.anchoredPosition = new Vector2(buttonConfig.x, -warningY);
        warningRect.sizeDelta = new Vector2(buttonConfig.width, warningHeight);
        
        warningText = warningObj.AddComponent<TextMeshProUGUI>();
        warningText.text = ""; // Sera rempli par UpdateWarningText
        warningText.fontSize = 20; // Taille augmentée pour meilleure visibilité
        warningText.color = new Color(1f, 0f, 0f, 1f); // Rouge opaque
        warningText.alignment = TextAlignmentOptions.Center;
        warningText.fontStyle = FontStyles.Bold; // Gras pour meilleure visibilité
        warningText.textWrappingMode = TextWrappingModes.Normal;
        warningText.overflowMode = TextOverflowModes.Overflow;
        warningText.raycastTarget = false; // Ne pas bloquer les clics
        if (textFont != null) warningText.font = textFont;
        
        // S'assurer que le texte est au-dessus de tout
        warningObj.transform.SetAsLastSibling();
        
        LogVerbose($"[QuestPreviewPanel] ✅ WarningText créé - Position: ({buttonConfig.x}, {-warningY}), Taille: {buttonConfig.width}x{warningHeight}, Bouton Y: {buttonsConfig.y}");
        
        // Masquer par défaut (sera activé par UpdateWarningText si nécessaire)
        warningObj.SetActive(false);
    }
    
    /// <summary>
    /// Sauvegarde le last_completed_game pour la difficulté sélectionnée dans PlayerPrefs
    /// Cela permet au MapManager de se positionner sur le step suivant lors de la reprise
    /// IMPORTANT: Définit aussi un flag pour indiquer qu'on lance depuis le panel (pas une navigation interne)
    /// </summary>
    private void SaveLastCompletedGameForSelectedDifficulty()
    {
        // Marquer qu'on lance depuis le panel de prévisualisation
        // Cela indique à MapManager d'utiliser EXCLUSIVEMENT last_completed_game et d'ignorer les anciens CurrentStepId
        PlayerPrefs.SetInt("LaunchedFromPreview_" + currentQuestId, 1);
        
        // Nettoyer les anciens CurrentStepId pour éviter les conflits
        // (on veut utiliser UNIQUEMENT last_completed_game comme source de vérité)
        PlayerPrefs.DeleteKey("CurrentStepId_" + currentQuestId);
        PlayerPrefs.DeleteKey("NextStepId_" + currentQuestId);
        
        if (currentUserProgress == null || currentUserProgress.Count == 0)
        {
            LogVerbose($"[QuestPreviewPanel] 📊 Pas de progression utilisateur - last_completed_game = 0 (démarrage au début)");
            PlayerPrefs.SetInt("LastCompletedGame_" + currentQuestId, 0);
            PlayerPrefs.Save();
            return;
        }
        
        // Récupérer le nom de la difficulté sélectionnée
        var diffConfig = config.infoCards.difficulty;
        if (selectedDifficultyIndex < 0 || selectedDifficultyIndex >= diffConfig.options.Length)
        {
            Debug.LogWarning($"[QuestPreviewPanel] ⚠️ Index de difficulté invalide: {selectedDifficultyIndex}");
            PlayerPrefs.SetInt("LastCompletedGame_" + currentQuestId, 0);
            PlayerPrefs.Save();
            return;
        }
        
        string selectedDifficulty = diffConfig.options[selectedDifficultyIndex];
        
        // Trouver la progression pour cette difficulté
        QuestUserProgress progress = currentUserProgress.Find(p => p.difficulty == selectedDifficulty);
        
        if (progress != null && progress.last_completed_game > 0)
        {
            PlayerPrefs.SetInt("LastCompletedGame_" + currentQuestId, progress.last_completed_game);
            LogVerbose($"[QuestPreviewPanel] 📊 last_completed_game sauvegardé: {progress.last_completed_game} pour difficulté '{selectedDifficulty}' → reprise au step suivant");
        }
        else
        {
            PlayerPrefs.SetInt("LastCompletedGame_" + currentQuestId, 0);
            LogVerbose($"[QuestPreviewPanel] 📊 Pas de last_completed_game pour difficulté '{selectedDifficulty}' → démarrage au début");
        }
        
        PlayerPrefs.Save();
    }
    
    /// <summary>
    /// Appelle l'endpoint de reset de la progression courante puis lance la quête
    /// </summary>
    private IEnumerator ResetCurrentProgressAndStartQuest()
    {
        // Construire l'URL de reset
        string resetUrl = GeneralConfigManager.Instance?.GetQuestResetCurrentApiUrl(currentQuestNumericId);
        
        if (string.IsNullOrEmpty(resetUrl))
        {
            Debug.LogWarning($"[QuestPreviewPanel] ⚠️ URL de reset invalide, lancement direct de la quête");
            ClosePanel();
            onStartQuest?.Invoke();
            yield break;
        }
        
        LogVerbose($"[QuestPreviewPanel] 🔄 Changement de difficulté détecté (de index {inProgressDifficultyIndex} vers {selectedDifficultyIndex})");
        LogVerbose($"[QuestPreviewPanel] 📡 Appel POST vers: {resetUrl}");
        
        // Désactiver le bouton pendant l'appel
        if (startButton != null)
        {
            startButton.interactable = false;
        }
        
        using (UnityWebRequest request = new UnityWebRequest(resetUrl, "POST"))
        {
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            
            // Ajouter le token d'authentification si disponible
            string authToken = UserDataManager.Instance?.token;
            if (!string.IsNullOrEmpty(authToken))
            {
                request.SetRequestHeader("Authorization", $"Bearer {authToken}");
            }
            
            yield return request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                LogVerbose($"[QuestPreviewPanel] ✅ Reset de la progression réussi: {request.downloadHandler.text}");
            }
            else
            {
                Debug.LogWarning($"[QuestPreviewPanel] ⚠️ Erreur lors du reset: {request.error} - Lancement de la quête quand même");
            }
        }
        
        // Réactiver le bouton (au cas où on reste sur le panel)
        if (startButton != null)
        {
            startButton.interactable = true;
        }
        
        // Lancer la quête
        ClosePanel();
        onStartQuest?.Invoke();
    }

    /// <summary>
    /// Cas "RECOMMENCER" : efface le badge (reset-by-difficulty) puis lance la quête au début sur la difficulté choisie.
    /// </summary>
    private IEnumerator ResetBadgeByDifficultyAndStartQuest(string difficulty)
    {
        // Désactiver le bouton pendant l'appel
        if (startButton != null) startButton.interactable = false;

        string url = GeneralConfigManager.Instance?.GetQuestAnswersResetByDifficultyUrl(currentQuestNumericId);
        if (string.IsNullOrEmpty(url))
        {
            Debug.LogWarning("[QuestPreviewPanel] ⚠️ URL reset-by-difficulty invalide, lancement annulé");
            if (startButton != null) startButton.interactable = true;
            yield break;
        }

        string jsonBody = $"{{\"difficulty\":\"{difficulty}\"}}";

        using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
        {
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");

            string token = UserDataManager.Instance != null ? (UserDataManager.Instance.token ?? "") : "";
            if (!string.IsNullOrEmpty(token))
                request.SetRequestHeader("Authorization", $"Bearer {token}");

            yield return request.SendWebRequest();

            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError($"[QuestPreviewPanel] ❌ Erreur reset-by-difficulty: {request.error} (HTTP {request.responseCode})");
                if (!string.IsNullOrEmpty(request.downloadHandler?.text))
                    Debug.LogError($"[QuestPreviewPanel] ❌ Body: {request.downloadHandler.text}");

                if (warningText != null)
                {
                    warningText.text = "Erreur lors de la suppression du badge. Réessayez.";
                    warningText.gameObject.SetActive(true);
                }

                if (startButton != null) startButton.interactable = true;
                yield break;
            }
        }

        // IMPORTANT: au restart, on veut commencer au début (pas reprendre via last_completed_game)
        SaveStartFromBeginningForSelectedDifficulty();

        ClosePanel();
        onStartQuest?.Invoke();
    }

    /// <summary>
    /// Forcer un démarrage depuis le début (LastCompletedGame = 0) tout en gardant la difficulté sélectionnée.
    /// </summary>
    private void SaveStartFromBeginningForSelectedDifficulty()
    {
        PlayerPrefs.SetInt("LaunchedFromPreview_" + currentQuestId, 1);
        PlayerPrefs.DeleteKey("CurrentStepId_" + currentQuestId);
        PlayerPrefs.DeleteKey("NextStepId_" + currentQuestId);
        PlayerPrefs.SetInt("LastCompletedGame_" + currentQuestId, 0);
        PlayerPrefs.Save();
    }
    
    /// <summary>
    /// Nettoie la mémoire en WebGL avant de charger une nouvelle scène (évite abort OOM)
    /// </summary>
    private IEnumerator CleanupMemoryBeforeSceneLoad()
    {
#if UNITY_WEBGL && !UNITY_EDITOR
        LogVerbose("[QuestPreviewPanel] 🧹 WebGL - Nettoyage mémoire avant rechargement...");
        
        // Purger les caches persistants
        try { if (RemoteSpriteCache.GetExisting() != null) RemoteSpriteCache.GetExisting().ClearAll(); } catch { }
        try { if (GameDataManager.Instance != null) GameDataManager.Instance.ClearAssetCache(); } catch { }
        
        // Attendre un frame pour que les Destroy() prennent effet
        yield return null;
        yield return new WaitForEndOfFrame();
        
        // Libérer les assets non référencés
        yield return Resources.UnloadUnusedAssets();
        try { System.GC.Collect(); } catch { }
        
        yield return null;
        LogVerbose("[QuestPreviewPanel] ✅ Nettoyage mémoire WebGL terminé");
#else
        yield return null;
#endif
    }
    
    /// <summary>
    /// Appelle l'endpoint pour skip la quête, ferme le panel et recharge la scène main
    /// </summary>
    private IEnumerator SkipQuestAndReload()
    {
        // Construire l'URL de skip
        string baseUrl = GeneralConfigManager.Instance?.GetApiBaseUrl();
        if (string.IsNullOrEmpty(baseUrl))
        {
            Debug.LogError("[QuestPreviewPanel] ❌ baseUrl non défini dans general-config.json");
            ClosePanel();
            
            // 🧹 Nettoyage mémoire WebGL avant rechargement
#if UNITY_WEBGL && !UNITY_EDITOR
            yield return CleanupMemoryBeforeSceneLoad();
#else
            yield return null;
#endif
            SceneManager.LoadScene("main");
            yield break;
        }
        
        if (currentQuestNumericId <= 0)
        {
            Debug.LogError($"[QuestPreviewPanel] ❌ currentQuestNumericId invalide: {currentQuestNumericId}");
            ClosePanel();
            
            // 🧹 Nettoyage mémoire WebGL avant rechargement
#if UNITY_WEBGL && !UNITY_EDITOR
            yield return CleanupMemoryBeforeSceneLoad();
#else
            yield return null;
#endif
            SceneManager.LoadScene("main");
            yield break;
        }
        
        string skipUrl = $"{baseUrl}/api/ujsa/quests/{currentQuestNumericId}/skip";
        
        LogVerbose($"[QuestPreviewPanel] 📡 Appel POST vers: {skipUrl}");
        
        using (UnityWebRequest request = new UnityWebRequest(skipUrl, "POST"))
        {
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            
            // Ajouter le token d'authentification si disponible
            string authToken = UserDataManager.Instance?.token;
            if (!string.IsNullOrEmpty(authToken))
            {
                request.SetRequestHeader("Authorization", $"Bearer {authToken}");
                LogVerbose($"[QuestPreviewPanel] 🔑 Token ajouté à la requête");
            }
            else
            {
                Debug.LogWarning("[QuestPreviewPanel] ⚠️ Pas de token d'authentification");
            }
            
            yield return request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                LogVerbose($"[QuestPreviewPanel] ✅ Quête skipée avec succès: {request.downloadHandler.text}");
            }
            else
            {
                Debug.LogWarning($"[QuestPreviewPanel] ⚠️ Erreur lors du skip: {request.error} - Rechargement de la scène quand même");
            }
        }
        
        // Fermer le panel
        ClosePanel();
        
        // 🧹 Nettoyage mémoire WebGL avant rechargement
        yield return CleanupMemoryBeforeSceneLoad();
        
        // Recharger la scène main
        LogVerbose("[QuestPreviewPanel] 🔄 Rechargement de la scène main...");
        SceneManager.LoadScene("main");
    }
    
    Button CreateActionButton(Transform parent, QuestPreviewButtonConfig buttonConfig, bool isStartButton, Action onClick)
    {
        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        var buttonStyle = generalConfig?.buttonStyles?.GetStyle(buttonConfig.style);
        var buttonsConfig = config.buttons;
        
        GameObject buttonObj = new GameObject(isStartButton ? "StartButton" : "SkipButton");
        buttonObj.transform.SetParent(parent, false);
        
        // Positionnement absolu
        RectTransform rect = buttonObj.AddComponent<RectTransform>();
        rect.anchorMin = new Vector2(0, 1); // Ancrer en haut à gauche
        rect.anchorMax = new Vector2(0, 1);
        rect.pivot = new Vector2(0, 1);
        rect.anchoredPosition = new Vector2(buttonConfig.x, -buttonsConfig.y);
        rect.sizeDelta = new Vector2(buttonConfig.width, buttonConfig.height);
        
        LogVerbose($"[QuestPreviewPanel] Bouton créé: {buttonConfig.label} - Position: ({buttonConfig.x}, {buttonsConfig.y}), Taille: {buttonConfig.width}x{buttonConfig.height}");
        
        Image image = buttonObj.AddComponent<Image>();
        
        // Utiliser le style du bouton avec les dimensions du config
        if (buttonStyle != null)
        {
            Color startColor = HexToColor(buttonStyle.gradient?.startColor ?? "#b87aff");
            Color endColor = HexToColor(buttonStyle.gradient?.endColor ?? "#7b4fbf");
            Color borderColor = HexToColor(buttonStyle.borderColor ?? "#f5ece5");
            
            image.sprite = CreateGradientSpriteWithBorder((int)buttonConfig.width, (int)buttonConfig.height, 
                buttonStyle.borderRadius, startColor, endColor, borderColor, buttonStyle.borderWidth);
            image.color = Color.white;
        }
        else
        {
            image.sprite = CreateRoundedColorSprite((int)buttonConfig.width, (int)buttonConfig.height, 25, HexToColor("#a95bfb"));
            image.color = Color.white;
        }
        image.type = Image.Type.Simple;
        image.raycastTarget = true; // S'assurer que le bouton bloque les clics

        // ✅ Ombre depuis le style de bouton (ex: validationDefault / cancelButtonDefault)
        // Sans ça, la shadow définie dans general-config.json n'est jamais visible dans le QuestPreviewPanel.
        if (buttonStyle?.shadow != null && buttonStyle.shadow.enabled)
        {
            Shadow shadow = buttonObj.GetComponent<Shadow>();
            if (shadow == null) shadow = buttonObj.AddComponent<Shadow>();
            shadow.effectColor = HexToColor(buttonStyle.shadow.color ?? "#00000040");
            shadow.effectDistance = new Vector2(buttonStyle.shadow.offsetX, -buttonStyle.shadow.offsetY);
        }
        
        Button button = buttonObj.AddComponent<Button>();
        button.targetGraphic = image;
        button.onClick.AddListener(() => onClick?.Invoke());
        
        // Texte du bouton
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(buttonObj.transform, false);
        
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.offsetMin = Vector2.zero;
        textRect.offsetMax = Vector2.zero;
        
        TextMeshProUGUI text = textObj.AddComponent<TextMeshProUGUI>();
        text.text = buttonConfig.label;
        text.fontSize = buttonStyle?.text?.fontSize ?? 22;
        text.color = HexToColor(buttonStyle?.text?.color ?? "#FFFFFF");
        text.alignment = TextAlignmentOptions.Center;
        text.fontStyle = FontStyles.Normal;
        if (titleFont != null) text.font = titleFont;
        
        return button;
    }
    
    void UpdateStartButtonState()
    {
        if (startButton == null || startButtonImage == null) return;
        
        bool isEnabled = selectedDifficultyIndex >= 0;
        startButton.interactable = isEnabled;
        
        // Changer le sprite selon l'état (enabled vs disabled)
        if (isEnabled && startButtonEnabledSprite != null)
        {
            startButtonImage.sprite = startButtonEnabledSprite;
            startButtonImage.color = Color.white;
        }
        else if (!isEnabled && startButtonDisabledSprite != null)
        {
            startButtonImage.sprite = startButtonDisabledSprite;
            startButtonImage.color = Color.white;
        }
        
        // Mettre à jour le texte d'avertissement
        UpdateWarningText();
        
        // Mettre à jour le label du bouton selon la progression
        if (startButtonText != null && isEnabled)
        {
            var diffConfig = config.infoCards.difficulty;
            if (selectedDifficultyIndex >= 0 && selectedDifficultyIndex < diffConfig.options.Length)
            {
                string selectedDifficulty = diffConfig.options[selectedDifficultyIndex];
                QuestUserProgress progress = currentUserProgress?.Find(p => p.difficulty == selectedDifficulty);
                
                if (progress != null)
                {
                    // Si la progression est à 100%, afficher "RECOMMENCER LA QUÊTE"
                    if (progress.completion == 100)
                    {
                        startButtonText.text = "RECOMMENCER LA QUETE";
                        LogVerbose($"[QuestPreviewPanel] 📊 Progression {progress.completion}% pour '{selectedDifficulty}' → Label: RECOMMENCER LA QUÊTE");
                    }
                    // Si la progression est entre 0 (exclus) et 100 (exclus), afficher "CONTINUER LA QUÊTE"
                    else if (progress.completion > 0 && progress.completion < 100)
                    {
                        startButtonText.text = "CONTINUER LA QUÊTE";
                        LogVerbose($"[QuestPreviewPanel] 📊 Progression {progress.completion}% pour '{selectedDifficulty}' → Label: CONTINUER LA QUÊTE");
                    }
                    else
                    {
                        // Sinon (completion == 0), utiliser le label par défaut de la config
                        startButtonText.text = config.buttons.startQuest.label;
                        LogVerbose($"[QuestPreviewPanel] 📊 Progression {progress.completion}% pour '{selectedDifficulty}' → Label: {config.buttons.startQuest.label}");
                    }
                }
                else
                {
                    // Pas de progression trouvée, utiliser le label par défaut
                    startButtonText.text = config.buttons.startQuest.label;
                    LogVerbose($"[QuestPreviewPanel] 📊 Aucune progression pour '{selectedDifficulty}' → Label: {config.buttons.startQuest.label}");
                }
            }
        }
        
        LogVerbose($"[QuestPreviewPanel] Bouton état: {(isEnabled ? "ENABLED (validationPurple)" : "DISABLED (secondaryAction)")}");
    }
    
    void UpdateWarningText()
    {
        if (warningText == null)
        {
            Debug.LogWarning("[QuestPreviewPanel] ⚠️ warningText est null dans UpdateWarningText()");
            return;
        }
        
        var diffConfig = config.infoCards.difficulty;
        if (diffConfig == null || diffConfig.options == null)
        {
            Debug.LogWarning("[QuestPreviewPanel] ⚠️ diffConfig ou options est null");
            warningText.gameObject.SetActive(false);
            return;
        }
        
        // Cas 1: Aucun niveau sélectionné
        if (selectedDifficultyIndex < 0)
        {
            warningText.text = "Sélectionnez un niveau de difficulté avant de commencer";
            warningText.gameObject.SetActive(true);
            LogVerbose("[QuestPreviewPanel] ⚠️ Avertissement affiché: Aucun niveau sélectionné");
            return;
        }
        
        if (selectedDifficultyIndex >= diffConfig.options.Length)
        {
            Debug.LogWarning($"[QuestPreviewPanel] ⚠️ Index invalide: {selectedDifficultyIndex} >= {diffConfig.options.Length}");
            warningText.gameObject.SetActive(false);
            return;
        }
        
        string selectedDifficulty = diffConfig.options[selectedDifficultyIndex];
        QuestUserProgress selectedProgress = currentUserProgress?.Find(p => p.difficulty == selectedDifficulty);
        
        LogVerbose($"[QuestPreviewPanel] 📊 UpdateWarningText - selectedDifficulty: {selectedDifficulty}, progress: {selectedProgress?.completion ?? -1}%, inProgressIndex: {inProgressDifficultyIndex}");
        
        // Cas 2: Completion = 100% pour le niveau sélectionné
        if (selectedProgress != null && selectedProgress.completion == 100)
        {
            string warningMessage = "Recommencer ce niveau de difficulté va effacer votre badge";
            warningText.text = warningMessage;
            warningText.gameObject.SetActive(true);
            LogVerbose($"[QuestPreviewPanel] ⚠️ Avertissement affiché: Completion 100% pour {selectedDifficulty}");
            LogVerbose($"[QuestPreviewPanel] 📝 Texte: '{warningMessage}', Active: {warningText.gameObject.activeSelf}, ActiveInHierarchy: {warningText.gameObject.activeInHierarchy}");
            LogVerbose($"[QuestPreviewPanel] 📐 Position: {warningText.rectTransform.anchoredPosition}, Taille: {warningText.rectTransform.sizeDelta}");
            return;
        }
        
        // Note: Le cas "partie en cours avec un autre niveau" est maintenant géré par une modale de confirmation
        // qui s'affiche au clic sur le bouton de démarrage (voir ShowConfirmationModal)
        
        // Aucun avertissement nécessaire
        warningText.gameObject.SetActive(false);
        LogVerbose("[QuestPreviewPanel] ✅ Aucun avertissement nécessaire");
    }
    
    // ========== Détection de partie en cours dans d'autres quêtes ==========
    
    /// <summary>
    /// Informations sur une partie en cours trouvée dans une autre quête
    /// </summary>
    private class OtherQuestInProgressInfo
    {
        public int questId;
        public string questTitle;
        public string difficulty;
        public int completion;
    }
    
    /// <summary>
    /// Cherche si une autre quête a une partie en cours (completion > 0 et < 100)
    /// </summary>
    /// <returns>Infos sur la partie en cours, ou null si aucune</returns>
    private OtherQuestInProgressInfo FindOtherQuestInProgress()
    {
        if (allQuestsData == null || allQuestsData.Count == 0)
            return null;
        
        foreach (var quest in allQuestsData)
        {
            // Ignorer la quête actuelle
            if (quest.id == currentQuestNumericId)
                continue;
            
            // Vérifier si cette quête a une partie en cours
            if (quest.user != null)
            {
                foreach (var progress in quest.user)
                {
                    if (progress.completion > 0 && progress.completion < 100)
                    {
                        LogVerbose($"[QuestPreviewPanel] 🔍 Partie en cours trouvée: Quête '{quest.title}' (id={quest.id}), {progress.difficulty}: {progress.completion}%");
                        return new OtherQuestInProgressInfo
                        {
                            questId = quest.id,
                            questTitle = quest.title,
                            difficulty = progress.difficulty,
                            completion = progress.completion
                        };
                    }
                }
            }
        }
        
        LogVerbose("[QuestPreviewPanel] 🔍 Aucune partie en cours dans d'autres quêtes");
        return null;
    }
    
    /// <summary>
    /// ID de la quête à reset (autre quête avec partie en cours)
    /// </summary>
    private int pendingResetOtherQuestId = 0;
    
    /// <summary>
    /// Affiche la modale de confirmation pour une partie en cours dans une autre quête
    /// </summary>
    private void ShowConfirmationModalForOtherQuest(OtherQuestInProgressInfo otherQuest)
    {
        pendingResetOtherQuestId = otherQuest.questId;
        string message = $"Vous avez déjà une partie en cours dans la quête \"{otherQuest.questTitle}\" niveau de difficulté {otherQuest.difficulty}. Elle va être effacée.";
        
        // Utiliser la modale générique avec un callback personnalisé
        ShowConfirmationModalWithCallback(message, () => {
            // Reset l'autre quête puis lance la nouvelle
            StartCoroutine(ResetOtherQuestAndStartCurrent());
        });
    }
    
    /// <summary>
    /// Reset la partie en cours d'une autre quête puis lance la quête actuelle
    /// </summary>
    private IEnumerator ResetOtherQuestAndStartCurrent()
    {
        if (pendingResetOtherQuestId <= 0)
        {
            Debug.LogWarning("[QuestPreviewPanel] ⚠️ Pas de quête à reset, lancement direct");
            SaveLastCompletedGameForSelectedDifficulty();
            ClosePanel();
            onStartQuest?.Invoke();
            yield break;
        }
        
        // Construire l'URL de reset pour l'autre quête
        string resetUrl = GeneralConfigManager.Instance?.GetQuestResetCurrentApiUrl(pendingResetOtherQuestId);
        
        if (string.IsNullOrEmpty(resetUrl))
        {
            Debug.LogWarning($"[QuestPreviewPanel] ⚠️ URL de reset invalide pour quête {pendingResetOtherQuestId}, lancement direct");
            SaveLastCompletedGameForSelectedDifficulty();
            ClosePanel();
            onStartQuest?.Invoke();
            yield break;
        }
        
        LogVerbose($"[QuestPreviewPanel] 🔄 Reset de la quête {pendingResetOtherQuestId} avant de lancer la quête {currentQuestNumericId}...");
        
        // Désactiver le bouton pendant la requête
        if (startButton != null)
        {
            startButton.interactable = false;
        }
        
        using (UnityWebRequest request = new UnityWebRequest(resetUrl, "POST"))
        {
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            
            string token = UserDataManager.Instance?.token;
            if (!string.IsNullOrEmpty(token))
            {
                request.SetRequestHeader("Authorization", $"Bearer {token}");
            }
            
            yield return request.SendWebRequest();
            
            if (request.result == UnityWebRequest.Result.Success)
            {
                LogVerbose($"[QuestPreviewPanel] ✅ Quête {pendingResetOtherQuestId} reset avec succès");
            }
            else
            {
                Debug.LogWarning($"[QuestPreviewPanel] ⚠️ Erreur lors du reset de la quête {pendingResetOtherQuestId}: {request.error}");
                // On continue quand même pour lancer la nouvelle quête
            }
        }
        
        // Réactiver le bouton
        if (startButton != null)
        {
            startButton.interactable = true;
        }
        
        // Réinitialiser l'ID de quête à reset
        pendingResetOtherQuestId = 0;
        
        // Maintenant lancer la quête actuelle
        SaveLastCompletedGameForSelectedDifficulty();
        ClosePanel();
        onStartQuest?.Invoke();
    }
    
    // ========== Modale de Confirmation ==========
    
    /// <summary>
    /// Callback à appeler lors de la confirmation de la modale
    /// </summary>
    private Action pendingConfirmationCallback;
    
    /// <summary>
    /// Affiche une modale de confirmation avec le style alertPanel (pour changement de difficulté dans la même quête)
    /// </summary>
    void ShowConfirmationModal(string message)
    {
        // Utiliser le callback par défaut pour la même quête
        ShowConfirmationModalWithCallback(message, () => {
            // Sauvegarder et lancer la quête
            SaveLastCompletedGameForSelectedDifficulty();
            StartCoroutine(ResetCurrentProgressAndStartQuest());
        });
    }
    
    /// <summary>
    /// Affiche une modale de confirmation avec un callback personnalisé
    /// </summary>
    void ShowConfirmationModalWithCallback(string message, Action onConfirm)
    {
        // Fermer une éventuelle modale existante
        CloseConfirmationModal();
        
        pendingConfirmationMessage = message;
        pendingConfirmationCallback = onConfirm;
        
        // Récupérer le style alertPanel
        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        var panelStyle = generalConfig?.panelStyles?.GetStyle("alertPanel");
        
        // Dimensions de la modale (depuis la config ou valeurs par défaut)
        float modalWidth = (panelStyle?.width > 0) ? panelStyle.width : 600f;
        float modalHeight = (panelStyle?.height > 0) ? panelStyle.height : 600f;
        float headerHeight = panelStyle?.header?.height ?? 140f;
        float cornerRadius = panelStyle?.cornerRadius ?? 40f;
        
        // Propriétés du header
        string headerTitleText = panelStyle?.header?.titleText ?? "ATTENTION";
        float headerTitleFontSize = panelStyle?.header?.titleFontSize ?? 32f;
        string headerTitleColor = panelStyle?.header?.titleColor ?? "#64477f";
        float closeButtonSize = panelStyle?.header?.closeButton?.size ?? 70f;
        float closeButtonMarginRight = panelStyle?.header?.closeButton?.marginRight ?? 30f;
        
        // Propriétés du contenu
        float contentFontSize = panelStyle?.content?.fontSize ?? 22f;
        string contentTextColor = panelStyle?.content?.textColor ?? "#64477f";
        
        // Styles des boutons
        string confirmButtonStyle = panelStyle?.buttons?.confirmButtonStyle ?? "validationDefault";
        string cancelButtonStyle = panelStyle?.buttons?.cancelButtonStyle ?? "validationBeige";
        
        // Propriétés de l'overlay
        bool overlayEnabled = panelStyle?.overlay?.enabled ?? true;
        string overlayColorHex = panelStyle?.overlay?.color ?? "#000000";
        float overlayOpacity = panelStyle?.overlay?.opacity ?? 0.5f;
        
        // Créer le conteneur de la modale (couvre tout l'écran)
        confirmationModal = new GameObject("ConfirmationModal");
        confirmationModal.transform.SetParent(panelRoot.transform, false);
        
        // Configurer le RectTransform pour couvrir tout l'écran
        RectTransform modalContainerRect = confirmationModal.AddComponent<RectTransform>();
        modalContainerRect.anchorMin = Vector2.zero;
        modalContainerRect.anchorMax = Vector2.one;
        modalContainerRect.offsetMin = Vector2.zero;
        modalContainerRect.offsetMax = Vector2.zero;
        
        // L'amener au premier plan
        confirmationModal.transform.SetAsLastSibling();
        
        // Overlay sombre pour bloquer les clics en dessous (si activé)
        if (overlayEnabled)
        {
            GameObject overlayObj = new GameObject("ModalOverlay");
            overlayObj.transform.SetParent(confirmationModal.transform, false);
            
            RectTransform overlayRect = overlayObj.AddComponent<RectTransform>();
            overlayRect.anchorMin = Vector2.zero;
            overlayRect.anchorMax = Vector2.one;
            overlayRect.offsetMin = Vector2.zero;
            overlayRect.offsetMax = Vector2.zero;
            
            Image overlayImage = overlayObj.AddComponent<Image>();
            Color overlayColor = HexToColor(overlayColorHex);
            overlayImage.color = new Color(overlayColor.r, overlayColor.g, overlayColor.b, overlayOpacity);
            overlayImage.raycastTarget = true; // Bloque les clics sur les éléments en dessous
            
            LogVerbose($"[QuestPreviewPanel] 🌑 Overlay créé - Couleur: {overlayColorHex}, Opacité: {overlayOpacity}");
        }
        
        // Panel principal
        GameObject panelObj = new GameObject("ModalPanel");
        panelObj.transform.SetParent(confirmationModal.transform, false);
        
        RectTransform panelRect = panelObj.AddComponent<RectTransform>();
        panelRect.anchorMin = new Vector2(0.5f, 0.5f);
        panelRect.anchorMax = new Vector2(0.5f, 0.5f);
        panelRect.pivot = new Vector2(0.5f, 0.5f);
        panelRect.sizeDelta = new Vector2(modalWidth, modalHeight);
        panelRect.anchoredPosition = Vector2.zero;
        
        // Fond du panel avec coins arrondis
        Image panelBg = panelObj.AddComponent<Image>();
        Color bgColor = HexToColor(panelStyle?.backgroundColor ?? "#f5ece5");
        panelBg.sprite = CreateRoundedColorSprite((int)modalWidth, (int)modalHeight, cornerRadius, bgColor);
        panelBg.type = Image.Type.Simple;
        panelBg.raycastTarget = true;
        
        // Header
        GameObject headerObj = new GameObject("Header");
        headerObj.transform.SetParent(panelObj.transform, false);
        
        RectTransform headerRect = headerObj.AddComponent<RectTransform>();
        headerRect.anchorMin = new Vector2(0, 1);
        headerRect.anchorMax = new Vector2(1, 1);
        headerRect.pivot = new Vector2(0.5f, 1);
        headerRect.sizeDelta = new Vector2(0, headerHeight);
        headerRect.anchoredPosition = Vector2.zero;
        
        Image headerBg = headerObj.AddComponent<Image>();
        Color headerColor = HexToColor(panelStyle?.header?.backgroundColor ?? "#dbc3b7");
        float headerCornerRadius = panelStyle?.header?.cornerRadius ?? 40f;
        headerBg.sprite = CreateRoundedTopSprite((int)modalWidth, (int)headerHeight, headerCornerRadius, headerColor);
        headerBg.type = Image.Type.Simple;
        
        // Titre du header
        GameObject titleObj = new GameObject("Title");
        titleObj.transform.SetParent(headerObj.transform, false);
        
        RectTransform titleRect = titleObj.AddComponent<RectTransform>();
        titleRect.anchorMin = new Vector2(0, 0);
        titleRect.anchorMax = new Vector2(1, 1);
        titleRect.sizeDelta = Vector2.zero;
        // Centrer le titre par rapport à toute la largeur du panel (et non l'espace restant).
        // On réserve symétriquement l'espace du bouton de fermeture (droite) aussi à gauche,
        // afin que le centre géométrique du titre reste le centre du panel.
        float reservedSide = closeButtonSize + closeButtonMarginRight;
        titleRect.offsetMin = new Vector2(20 + reservedSide, 0);
        titleRect.offsetMax = new Vector2(-(20 + reservedSide), 0);
        
        TextMeshProUGUI titleText = titleObj.AddComponent<TextMeshProUGUI>();
        titleText.text = headerTitleText;
        titleText.fontSize = headerTitleFontSize;
        titleText.color = HexToColor(headerTitleColor);
        titleText.alignment = TextAlignmentOptions.Center;
        titleText.fontStyle = FontStyles.Bold;
        if (titleFont != null) titleText.font = titleFont;
        
        // Bouton fermer (croix)
        CreateModalCloseButton(headerObj.transform, closeButtonSize, closeButtonMarginRight);
        
        // Contenu (message)
        GameObject contentObj = new GameObject("Content");
        contentObj.transform.SetParent(panelObj.transform, false);
        
        RectTransform contentRect = contentObj.AddComponent<RectTransform>();
        contentRect.anchorMin = new Vector2(0, 0);
        contentRect.anchorMax = new Vector2(1, 1);
        contentRect.pivot = new Vector2(0.5f, 0.5f);
        contentRect.offsetMin = new Vector2(30, 100); // Espace pour les boutons en bas
        contentRect.offsetMax = new Vector2(-30, -headerHeight - 20);
        
        TextMeshProUGUI messageText = contentObj.AddComponent<TextMeshProUGUI>();
        messageText.text = message;
        messageText.fontSize = contentFontSize;
        messageText.color = HexToColor(contentTextColor);
        messageText.alignment = TextAlignmentOptions.Center;
        messageText.textWrappingMode = TextWrappingModes.Normal;
        if (textFont != null) messageText.font = textFont;
        
        // Conteneur des boutons
        GameObject buttonsContainer = new GameObject("ButtonsContainer");
        buttonsContainer.transform.SetParent(panelObj.transform, false);
        
        RectTransform buttonsRect = buttonsContainer.AddComponent<RectTransform>();
        buttonsRect.anchorMin = new Vector2(0.5f, 0);
        buttonsRect.anchorMax = new Vector2(0.5f, 0);
        buttonsRect.pivot = new Vector2(0.5f, 0);
        buttonsRect.sizeDelta = new Vector2(modalWidth - 60, 80);
        buttonsRect.anchoredPosition = new Vector2(0, 30);
        
        HorizontalLayoutGroup layout = buttonsContainer.AddComponent<HorizontalLayoutGroup>();
        layout.spacing = 20;
        layout.childAlignment = TextAnchor.MiddleCenter;
        layout.childControlWidth = false;
        layout.childControlHeight = false;
        layout.childForceExpandWidth = false;
        layout.childForceExpandHeight = false;
        
        // Bouton Annuler (style depuis la config)
        CreateModalButton(buttonsContainer.transform, "ANNULER", cancelButtonStyle, () => {
            CloseConfirmationModal();
            pendingResetOtherQuestId = 0; // Reset au cas où
        });
        
        // Bouton Confirmer (style depuis la config) - utilise le callback passé en paramètre
        CreateModalButton(buttonsContainer.transform, "CONFIRMER", confirmButtonStyle, () => {
            CloseConfirmationModal();
            pendingConfirmationCallback?.Invoke();
        });
        
        LogVerbose($"[QuestPreviewPanel] 📋 Modale de confirmation affichée");
    }
    
    /// <summary>
    /// Crée le bouton fermer (croix) dans le header de la modale
    /// </summary>
    void CreateModalCloseButton(Transform parent, float size = 70f, float marginRight = 30f)
    {
        GameObject closeObj = new GameObject("CloseButton");
        closeObj.transform.SetParent(parent, false);
        
        RectTransform closeRect = closeObj.AddComponent<RectTransform>();
        closeRect.anchorMin = new Vector2(1, 0.5f);
        closeRect.anchorMax = new Vector2(1, 0.5f);
        closeRect.pivot = new Vector2(1, 0.5f);
        closeRect.sizeDelta = new Vector2(size, size);
        closeRect.anchoredPosition = new Vector2(-marginRight, 0);
        
        // Utiliser le même sprite que le bouton fermer principal si disponible
        Image closeImage = closeObj.AddComponent<Image>();
        if (cachedCloseButtonSprite != null)
        {
            closeImage.sprite = cachedCloseButtonSprite;
        }
        else
        {
            // Fallback: créer un bouton avec un X
            int sizeInt = (int)size;
            closeImage.sprite = CreateRoundedColorSprite(sizeInt, sizeInt, size / 2f, HexToColor("#64477f"));
        }
        closeImage.raycastTarget = true;
        
        Button closeButton = closeObj.AddComponent<Button>();
        closeButton.targetGraphic = closeImage;
        closeButton.onClick.AddListener(() => {
            CloseConfirmationModal();
        });
        
        // Texte X si pas de sprite
        if (cachedCloseButtonSprite == null)
        {
            GameObject xTextObj = new GameObject("XText");
            xTextObj.transform.SetParent(closeObj.transform, false);
            
            RectTransform xRect = xTextObj.AddComponent<RectTransform>();
            xRect.anchorMin = Vector2.zero;
            xRect.anchorMax = Vector2.one;
            xRect.sizeDelta = Vector2.zero;
            
            TextMeshProUGUI xText = xTextObj.AddComponent<TextMeshProUGUI>();
            xText.text = "✕";
            xText.fontSize = size * 0.5f; // Taille du texte proportionnelle au bouton
            xText.color = Color.white;
            xText.alignment = TextAlignmentOptions.Center;
            xText.raycastTarget = false;
        }
    }
    
    /// <summary>
    /// Crée un bouton pour la modale avec le style spécifié
    /// </summary>
    void CreateModalButton(Transform parent, string label, string styleName, Action onClick)
    {
        var generalConfig = GeneralConfigManager.Instance?.GetConfig();
        var buttonStyle = generalConfig?.buttonStyles?.GetStyle(styleName);
        
        float btnWidth = buttonStyle?.width ?? 200f;
        float btnHeight = buttonStyle?.height ?? 60f;
        float borderRadius = buttonStyle?.borderRadius ?? 30f;
        float borderWidth = buttonStyle?.borderWidth ?? 0f;
        
        GameObject buttonObj = new GameObject($"Button_{label}");
        buttonObj.transform.SetParent(parent, false);
        
        RectTransform buttonRect = buttonObj.AddComponent<RectTransform>();
        buttonRect.sizeDelta = new Vector2(btnWidth, btnHeight);
        
        LayoutElement layoutElement = buttonObj.AddComponent<LayoutElement>();
        layoutElement.preferredWidth = btnWidth;
        layoutElement.preferredHeight = btnHeight;
        
        // Image du bouton
        Image buttonImage = buttonObj.AddComponent<Image>();
        
        if (buttonStyle?.gradient?.enabled == true)
        {
            Color startColor = HexToColor(buttonStyle.gradient.startColor ?? "#CE9BFD");
            Color endColor = HexToColor(buttonStyle.gradient.endColor ?? "#9A2DFF");
            Color borderColor = HexToColor(buttonStyle.borderColor ?? "#f5ece5");
            buttonImage.sprite = CreateGradientSpriteWithBorder((int)btnWidth, (int)btnHeight, borderRadius, startColor, endColor, borderColor, borderWidth);
        }
        else
        {
            Color bgColor = HexToColor(buttonStyle?.gradient?.startColor ?? "#f5ece5");
            Color borderColor = HexToColor(buttonStyle?.borderColor ?? "#cc9442");
            buttonImage.sprite = CreateRoundedBorderSprite((int)btnWidth, (int)btnHeight, borderRadius, bgColor, borderColor, borderWidth);
        }
        buttonImage.type = Image.Type.Simple;
        buttonImage.raycastTarget = true;

        // ✅ Ombre depuis le style de bouton (ex: cancelButtonDefault / validationDefault)
        if (buttonStyle?.shadow != null && buttonStyle.shadow.enabled)
        {
            Shadow shadow = buttonObj.GetComponent<Shadow>();
            if (shadow == null) shadow = buttonObj.AddComponent<Shadow>();
            shadow.effectColor = HexToColor(buttonStyle.shadow.color ?? "#00000040");
            shadow.effectDistance = new Vector2(buttonStyle.shadow.offsetX, -buttonStyle.shadow.offsetY);
        }
        
        // Bouton
        Button button = buttonObj.AddComponent<Button>();
        button.targetGraphic = buttonImage;
        button.onClick.AddListener(() => onClick?.Invoke());
        
        // Texte du bouton
        GameObject textObj = new GameObject("Text");
        textObj.transform.SetParent(buttonObj.transform, false);
        
        RectTransform textRect = textObj.AddComponent<RectTransform>();
        textRect.anchorMin = Vector2.zero;
        textRect.anchorMax = Vector2.one;
        textRect.sizeDelta = Vector2.zero;
        
        TextMeshProUGUI buttonText = textObj.AddComponent<TextMeshProUGUI>();
        buttonText.text = label;
        buttonText.fontSize = buttonStyle?.text?.fontSize ?? 22f;
        buttonText.color = HexToColor(buttonStyle?.text?.color ?? "#FFFFFF");
        buttonText.alignment = TextAlignmentOptions.Center;
        buttonText.fontStyle = FontStyles.Bold;
        buttonText.raycastTarget = false;
        if (titleFont != null) buttonText.font = titleFont;
    }
    
    /// <summary>
    /// Ferme la modale de confirmation
    /// </summary>
    void CloseConfirmationModal()
    {
        if (confirmationModal != null)
        {
            Destroy(confirmationModal);
            confirmationModal = null;
            pendingConfirmationMessage = null;
            LogVerbose("[QuestPreviewPanel] 📋 Modale de confirmation fermée");
        }
    }
    
    /// <summary>
    /// Crée un sprite avec des coins arrondis et une bordure
    /// </summary>
    Sprite CreateRoundedBorderSprite(int width, int height, float radius, Color fillColor, Color borderColor, float borderWidth)
    {
        Texture2D texture = new Texture2D(width, height);
        Color[] pixels = new Color[width * height];
        
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float distanceFromEdge = DistanceFromRoundedRect(x, y, width, height, radius);
                
                if (distanceFromEdge > 0)
                {
                    // En dehors du rectangle arrondi
                    pixels[y * width + x] = Color.clear;
                }
                else if (distanceFromEdge > -borderWidth)
                {
                    // Dans la bordure
                    pixels[y * width + x] = borderColor;
                }
                else
                {
                    // À l'intérieur
                    pixels[y * width + x] = fillColor;
                }
            }
        }
        
        texture.SetPixels(pixels);
        texture.Apply();
        
        dynamicTextures.Add(texture);
        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f);
        dynamicSprites.Add(sprite);
        return sprite;
    }
    
    /// <summary>
    /// Calcule la distance signée depuis le bord d'un rectangle arrondi
    /// </summary>
    float DistanceFromRoundedRect(int x, int y, int width, int height, float radius)
    {
        // Vérifier chaque coin
        if (x < radius && y < radius)
        {
            // Coin inférieur gauche
            return Mathf.Sqrt((radius - x) * (radius - x) + (radius - y) * (radius - y)) - radius;
        }
        else if (x > width - radius && y < radius)
        {
            // Coin inférieur droit
            return Mathf.Sqrt((x - (width - radius)) * (x - (width - radius)) + (radius - y) * (radius - y)) - radius;
        }
        else if (x < radius && y > height - radius)
        {
            // Coin supérieur gauche
            return Mathf.Sqrt((radius - x) * (radius - x) + (y - (height - radius)) * (y - (height - radius))) - radius;
        }
        else if (x > width - radius && y > height - radius)
        {
            // Coin supérieur droit
            return Mathf.Sqrt((x - (width - radius)) * (x - (width - radius)) + (y - (height - radius)) * (y - (height - radius))) - radius;
        }
        
        // Bords droits
        float distX = Mathf.Min(x, width - x);
        float distY = Mathf.Min(y, height - y);
        return -Mathf.Min(distX, distY);
    }
    
    // ========== Utilitaires ==========
    
    // Méthode copiée de SettingsPanelBuilder - avec anti-aliasing propre
    Sprite CreateRoundedColorSprite(int width, int height, float radius, Color fillColor)
    {
        Texture2D texture = new Texture2D(width, height);
        Color[] pixels = new Color[width * height];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float alpha = 1f;

                // Coin supérieur gauche
                if (x < radius && y > height - radius)
                {
                    float dx = radius - x;
                    float dy = (height - radius) - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                        alpha = 0f;
                    else
                        alpha = 1f - Mathf.Clamp01((distance - radius + 1) / 1);
                }
                // Coin supérieur droit
                else if (x > width - radius && y > height - radius)
                {
                    float dx = x - (width - radius);
                    float dy = (height - radius) - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                        alpha = 0f;
                    else
                        alpha = 1f - Mathf.Clamp01((distance - radius + 1) / 1);
                }
                // Coin inférieur gauche
                else if (x < radius && y < radius)
                {
                    float dx = radius - x;
                    float dy = radius - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                        alpha = 0f;
                    else
                        alpha = 1f - Mathf.Clamp01((distance - radius + 1) / 1);
                }
                // Coin inférieur droit
                else if (x > width - radius && y < radius)
                {
                    float dx = x - (width - radius);
                    float dy = radius - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                        alpha = 0f;
                    else
                        alpha = 1f - Mathf.Clamp01((distance - radius + 1) / 1);
                }

                pixels[y * width + x] = new Color(fillColor.r, fillColor.g, fillColor.b, fillColor.a * alpha);
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();
        
        dynamicTextures.Add(texture);
        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f);
        dynamicSprites.Add(sprite);
        return sprite;
    }
    
    // Méthode copiée de SettingsPanelBuilder - coins arrondis uniquement en HAUT
    Sprite CreateRoundedTopSprite(int width, int height, float radius, Color fillColor)
    {
        Texture2D texture = new Texture2D(width, height);
        Color[] pixels = new Color[width * height];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float alpha = 1f;

                // Seulement les coins du HAUT sont arrondis
                // Coin supérieur gauche
                if (x < radius && y > height - radius)
                {
                    float dx = radius - x;
                    float dy = (height - radius) - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                        alpha = 0f;
                    else
                        alpha = 1f - Mathf.Clamp01((distance - radius + 1) / 1);
                }
                // Coin supérieur droit
                else if (x > width - radius && y > height - radius)
                {
                    float dx = x - (width - radius);
                    float dy = (height - radius) - y;
                    float distance = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distance > radius)
                        alpha = 0f;
                    else
                        alpha = 1f - Mathf.Clamp01((distance - radius + 1) / 1);
                }
                // Coins inférieurs : pas arrondis (alpha reste à 1)

                pixels[y * width + x] = new Color(fillColor.r, fillColor.g, fillColor.b, fillColor.a * alpha);
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();
        
        dynamicTextures.Add(texture);
        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f);
        dynamicSprites.Add(sprite);
        return sprite;
    }
    
    Sprite CreateGradientSpriteWithBorder(int width, int height, float radius, Color startColor, Color endColor, Color borderColor, float borderWidth)
    {
        Texture2D texture = new Texture2D(width, height);
        Color[] pixels = new Color[width * height];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                float alpha = 1f;
                bool isBorder = false;
                
                // Vérifier si on est dans la bordure
                if (x < borderWidth || x >= width - borderWidth || y < borderWidth || y >= height - borderWidth)
                {
                    isBorder = true;
                }

                // Coins arrondis
                float distanceFromCorner = float.MaxValue;
                
                // Coin supérieur gauche
                if (x < radius && y > height - radius)
                {
                    float dx = radius - x;
                    float dy = (height - radius) - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }
                // Coin supérieur droit
                else if (x > width - radius && y > height - radius)
                {
                    float dx = x - (width - radius);
                    float dy = (height - radius) - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }
                // Coin inférieur gauche
                else if (x < radius && y < radius)
                {
                    float dx = radius - x;
                    float dy = radius - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }
                // Coin inférieur droit
                else if (x > width - radius && y < radius)
                {
                    float dx = x - (width - radius);
                    float dy = radius - y;
                    distanceFromCorner = Mathf.Sqrt(dx * dx + dy * dy);
                    if (distanceFromCorner > radius) alpha = 0f;
                    else if (distanceFromCorner > radius - borderWidth) isBorder = true;
                }

                Color pixelColor;
                if (isBorder && alpha > 0)
                {
                    pixelColor = borderColor;
                }
                else
                {
                    // Dégradé vertical
                    float t = (float)y / height;
                    pixelColor = Color.Lerp(endColor, startColor, t);
                }
                
                pixels[y * width + x] = new Color(pixelColor.r, pixelColor.g, pixelColor.b, pixelColor.a * alpha);
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();
        
        // Tracker la texture pour la nettoyer plus tard
        dynamicTextures.Add(texture);
        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f);
        dynamicSprites.Add(sprite);
        return sprite;
    }
    
    Color HexToColor(string hex)
    {
        if (string.IsNullOrEmpty(hex)) return Color.white;
        
        hex = hex.TrimStart('#');
        
        if (hex.Length == 6)
        {
            hex = hex + "FF";
        }
        
        if (hex.Length == 8)
        {
            byte r = byte.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
            byte g = byte.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
            byte b = byte.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
            byte a = byte.Parse(hex.Substring(6, 2), System.Globalization.NumberStyles.HexNumber);
            
            return new Color(r / 255f, g / 255f, b / 255f, a / 255f);
        }
        
        return Color.white;
    }

    private void CleanupRuntimeResources(bool destroyPanelRoot)
    {
        // Fermer la modale de confirmation si elle est ouverte
        CloseConfirmationModal();
        
        // Nettoyer la vidéo (stop + detach + release RT)
        if (currentVideoPlayer != null)
        {
            try
            {
                currentVideoPlayer.prepareCompleted -= OnVideoPrepareCompleted;
                currentVideoPlayer.started -= OnVideoStarted;
                currentVideoPlayer.errorReceived -= OnVideoErrorReceived;
                currentVideoPlayer.loopPointReached -= OnVideoLoopPointReached;
            }
            catch { /* noop */ }

            if (currentVideoPlayer.isPlaying) currentVideoPlayer.Stop();
            currentVideoPlayer.targetTexture = null;
            currentVideoPlayer = null;
        }

        if (currentVideoRawImage != null)
        {
            currentVideoRawImage.texture = null;
            currentVideoRawImage = null;
        }

        if (currentVideoRenderTexture != null)
        {
            try { currentVideoRenderTexture.Release(); } catch { /* noop */ }
            Destroy(currentVideoRenderTexture);
            currentVideoRenderTexture = null;
        }

        for (int i = 0; i < dynamicRenderTextures.Count; i++)
        {
            RenderTexture rt = dynamicRenderTextures[i];
            if (rt != null)
            {
                try { rt.Release(); } catch { /* noop */ }
                Destroy(rt);
            }
        }
        dynamicRenderTextures.Clear();

        // Nettoyer sprites/textures dynamiques
        for (int i = 0; i < dynamicSprites.Count; i++)
        {
            Sprite sp = dynamicSprites[i];
            if (sp != null) Destroy(sp);
        }
        dynamicSprites.Clear();

        for (int i = 0; i < dynamicTextures.Count; i++)
        {
            Texture2D tex = dynamicTextures[i];
            if (tex != null) Destroy(tex);
        }
        dynamicTextures.Clear();

        pendingFormationArrowTargets.Clear();
        isLoadingFormationArrowSprite = false;

        if (destroyPanelRoot && panelRoot != null)
        {
            Destroy(panelRoot);
            panelRoot = null;
        }

        // NOTE(WebGL): On évite ici GC.Collect / Resources.UnloadUnusedAssets() qui peuvent provoquer
        // des comportements instables (boucles internes / stack overflow) selon le runtime WebGL.
        // On détruit déjà explicitement les ressources GPU/CPU ci-dessus.
    }

    private void CancelBuild()
    {
        if (buildRoutine != null)
        {
            StopCoroutine(buildRoutine);
            buildRoutine = null;
        }
    }
}

