-- ============================================================
-- Asylum Compagnon — Core.lua
-- Initialisation, variables sauvegardées, bouton minimap,
-- événements (équipement/talents/or/sacs/groupe), commandes.
-- ============================================================

local ADDON_NAME, NS = ...

-- Couleur d'accent (violet moderne) et icône (fileID vérifié)
NS.ACCENT = "a78bfa"
NS.ICON_ID = 254109 -- achievement_dungeon_ulduarraid_irongolem_01

-- Seuil « stock bas » d'un consommable suivi (≤ → jaune + avertissement).
NS.LOW_STOCK = 5

-- Consommables suivis dans le menu Inventaire.
-- ids = tous les itemID de l'objet (les paliers de qualité ont parfois
-- des ID distincts : on additionne). Le 1er ID sert de clé + d'icône.
-- Le nom affiché en jeu est localisé automatiquement (FR sur client FR).
-- cat : "flask" · "potion" · "food" · "oil" (sert au regroupement de l'inventaire)
NS.TRACKED_ITEMS = {
    -- Flacons (Shattered Sun / Magisters / Blood Knights / Thalassian Resistance)
    { cat = "flask", ids = { 241326 },         fallback = "Flacon du Soleil brisé" },
    { cat = "flask", ids = { 241322, 241323 }, fallback = "Flacon des magistères" },
    { cat = "flask", ids = { 241324, 241325 }, fallback = "Flacon des chevaliers de sang" },
    { cat = "flask", ids = { 241320 },         fallback = "Flacon de résistance thalassienne" },
    -- Potions (EN: Light's Potential / Potion of Recklessness)
    { cat = "potion", ids = { 241309, 241308 }, fallback = "Potentiel de la Lumière" },
    { cat = "potion", ids = { 241288, 241289 }, fallback = "Potion de témérité" },
    -- Festins copieux (Harandar / Royal Roast / Crimson Calamari / Smoked Tetra
    -- / Wise Tails / Braised Blood Hunter)
    { cat = "food", ids = { 266996, 255846 }, fallback = "Célébration d'Harandar copieuse" }, -- + version non copieuse (Harandar Celebration)
    { cat = "food", ids = { 242747 },         fallback = "Rôti royal copieux" },
    { cat = "food", ids = { 242749 },         fallback = "Calmar cramoisi copieux" },
    { cat = "food", ids = { 242750 },         fallback = "Tétra fumé savoureux copieux" },
    { cat = "food", ids = { 242762 },         fallback = "Queues avisées copieuses" },
    { cat = "food", ids = { 242748 },         fallback = "Chasseur de sang braisé copieux" },
    { cat = "food", ids = { 242753 },         fallback = "Brochettes scintillantes copieuses" },     -- Hearty Glitter Skewers (vérifié wowhead)
    { cat = "food", ids = { 242765 },         fallback = "Délice du Puits de soleil copieux" },       -- Hearty Sunwell Delight (vérifié wowhead)
    { cat = "food", ids = { 242768, 242296 }, fallback = "Escalopes de sang-de-chardon copieuses" },  -- Hearty Bloodthistle-Wrapped Cutlets (242768 = motif +472 ; +242296 non-copieux en secours)
    -- Huiles d'arme (EN: Thalassian Phoenix Oil / Oil of Dawn)
    { cat = "oil", ids = { 243733, 243734 }, fallback = "Huile de phénix thalassienne" },
    { cat = "oil", ids = { 243735, 243736 }, fallback = "Huile de l'aube" },
    -- Divers (utilitaires partagés en groupe)
    { cat = "other", ids = { 132514 }, fallback = "Auto-marteau" }, -- Auto-Hammer : réparation de groupe
}

-- Sorts d'interruption → cooldown de base en secondes.
-- Table éditable : ajoute ici tout spellID manquant (les variants
-- d'un même kick ont des IDs différents, ex. Commandement du démon).
NS.INTERRUPTS = {
    [6552]   = 15, -- Guerrier : Volée de coups (Pummel)
    [96231]  = 15, -- Paladin : Réprimandes (Rebuke)
    [31935]  = 15, -- Paladin Prot : Bouclier du vengeur
    [147362] = 24, -- Chasseur : Tir de contre (Counter Shot)
    [187707] = 15, -- Chasseur Survie : Musellement (Muzzle)
    [1766]   = 15, -- Voleur : Coup de pied (Kick)
    [15487]  = 45, -- Prêtre Ombre : Silence
    [47528]  = 15, -- CdM : Gel de l'esprit (Mind Freeze)
    [221562] = 45, -- CdM Sang : Asphyxie (interrompt via étourdissement)
    [47476]  = 60, -- CdM : Strangulation (si talentée)
    [57994]  = 12, -- Chaman : Cisaillement des vents (Wind Shear)
    [2139]   = 24, -- Mage : Contresort (Counterspell)
    [19647]  = 24, -- Démoniste : Verrou de sort (chasseur corrompu)
    [119910] = 24, -- Démoniste : Verrou de sort (Commandement du démon)
    [132409] = 24, -- Démoniste : Verrou de sort (pacte de sacrifice)
    [89766]  = 30, -- Démoniste : Lancer de hache (gangregarde)
    [119914] = 30, -- Démoniste : Lancer de hache (Commandement du démon)
    [116705] = 15, -- Moine : Frappe du bras-lance (Spear Hand Strike)
    [106839] = 15, -- Druide : Coup de crâne (Skull Bash)
    [78675]  = 60, -- Druide Équilibre : Rayon solaire (Solar Beam)
    [183752] = 15, -- Chasseur de démons : Perturbation (Disrupt)
    [351338] = 40, -- Évocateur : Étouffement (Quell)
}

-- ============================================================
-- Cooldowns de groupe suivis (tracker esprit oRA3/OmniCD).
-- Table éditable : spellID → { cd (s), class, type, label, def }.
--   type : "off" offensif · "def" défensif perso · "ext" externe/raid
--          · "lust" Bloodlust & co · "res" résurrection de combat
--   def=true : coché par défaut (les autres sont dispo mais décochés).
-- Détecté via UNIT_SPELLCAST_SUCCEEDED (comme les kicks) puis diffusé
-- au groupe (message « D »). Spell IDs alignés sur oRA3 (BigWigs), à
-- ajuster en jeu si Midnight en modifie : /asylum cd liste tes CD.
-- ============================================================
NS.COOLDOWNS = {
    -- ----- Chevalier de la mort -----
    [51271]  = { cd = 45,  class = "DEATHKNIGHT", type = "off", label = "Pilier de givre",         def = true },
    [47568]  = { cd = 120, class = "DEATHKNIGHT", type = "off", label = "Runes renforcées" },
    [49028]  = { cd = 120, class = "DEATHKNIGHT", type = "off", label = "Arme runique dansante" },
    [42650]  = { cd = 480, class = "DEATHKNIGHT", type = "off", label = "Armée des morts" },
    [48792]  = { cd = 180, class = "DEATHKNIGHT", type = "def", label = "Force intransigeante",     def = true },
    [48707]  = { cd = 60,  class = "DEATHKNIGHT", type = "def", label = "Carapace anti-magie" },
    [55233]  = { cd = 90,  class = "DEATHKNIGHT", type = "def", label = "Sang vampirique" },
    [51052]  = { cd = 120, class = "DEATHKNIGHT", type = "ext", label = "Zone anti-magie",          def = true },
    [61999]  = { cd = 600, class = "DEATHKNIGHT", type = "res", label = "Résurrection d'allié",     def = true },

    -- ----- Chasseur de démons -----
    [191427] = { cd = 240, class = "DEMONHUNTER", type = "off", label = "Métamorphose",             def = true },
    [258925] = { cd = 90,  class = "DEMONHUNTER", type = "off", label = "Barrage de flammes" },
    [370965] = { cd = 90,  class = "DEMONHUNTER", type = "off", label = "La Traque" },
    [258860] = { cd = 40,  class = "DEMONHUNTER", type = "off", label = "Brèche d'essence" },
    [198589] = { cd = 60,  class = "DEMONHUNTER", type = "def", label = "Trouble",                  def = true },
    [187827] = { cd = 180, class = "DEMONHUNTER", type = "def", label = "Métamorphose (Veng.)" },
    [204021] = { cd = 60,  class = "DEMONHUNTER", type = "def", label = "Marque ardente" },
    [196718] = { cd = 300, class = "DEMONHUNTER", type = "ext", label = "Ténèbres",                 def = true },

    -- ----- Druide -----
    [194223] = { cd = 180, class = "DRUID", type = "off", label = "Alignement céleste",             def = true },
    [102560] = { cd = 180, class = "DRUID", type = "off", label = "Incarnation : Élune" },
    [106951] = { cd = 180, class = "DRUID", type = "off", label = "Berserk",                        def = true },
    [391528] = { cd = 120, class = "DRUID", type = "off", label = "Invocation des esprits" },
    [61336]  = { cd = 180, class = "DRUID", type = "def", label = "Instinct de survie",             def = true },
    [22812]  = { cd = 60,  class = "DRUID", type = "def", label = "Écorce" },
    [200851] = { cd = 60,  class = "DRUID", type = "def", label = "Rage du Dormeur" },
    [740]    = { cd = 180, class = "DRUID", type = "ext", label = "Tranquillité" },
    [102342] = { cd = 90,  class = "DRUID", type = "ext", label = "Écorce de fer" },
    [29166]  = { cd = 180, class = "DRUID", type = "ext", label = "Innervation" },
    [106898] = { cd = 120, class = "DRUID", type = "ext", label = "Rugissement fonceur" },
    [20484]  = { cd = 600, class = "DRUID", type = "res", label = "Renaissance",                    def = true },

    -- ----- Évocateur -----
    [375087] = { cd = 120, class = "EVOKER", type = "off", label = "Furie draconique",              def = true },
    [357210] = { cd = 120, class = "EVOKER", type = "off", label = "Grande inspiration" },
    [390386] = { cd = 300, class = "EVOKER", type = "lust", label = "Furie des Aspects",            def = true },
    [363916] = { cd = 90,  class = "EVOKER", type = "def", label = "Écailles d'obsidienne",         def = true },
    [374348] = { cd = 90,  class = "EVOKER", type = "def", label = "Flamme régénérante" },
    [363534] = { cd = 240, class = "EVOKER", type = "ext", label = "Rembobinage" },
    [374227] = { cd = 120, class = "EVOKER", type = "ext", label = "Zéphyr",                        def = true },
    [370960] = { cd = 180, class = "EVOKER", type = "ext", label = "Communion émeraude" },
    [357170] = { cd = 60,  class = "EVOKER", type = "ext", label = "Dilatation temporelle" },

    -- ----- Chasseur -----
    [19574]  = { cd = 90,  class = "HUNTER", type = "off", label = "Furie bestiale",                def = true },
    [288613] = { cd = 120, class = "HUNTER", type = "off", label = "Tir de précision",              def = true },
    [266779] = { cd = 120, class = "HUNTER", type = "off", label = "Assaut coordonné" },
    [359844] = { cd = 120, class = "HUNTER", type = "off", label = "Appel de la nature" },
    [360966] = { cd = 90,  class = "HUNTER", type = "off", label = "Fer de lance" },
    [186265] = { cd = 180, class = "HUNTER", type = "def", label = "Aspect de la tortue",           def = true },
    [109304] = { cd = 120, class = "HUNTER", type = "def", label = "Liesse" },
    [264735] = { cd = 180, class = "HUNTER", type = "def", label = "Survie du plus apte" },
    [264667] = { cd = 360, class = "HUNTER", type = "lust", label = "Rage primitive",               def = true },

    -- ----- Mage -----
    [190319] = { cd = 120, class = "MAGE", type = "off", label = "Combustion",                      def = true },
    [12472]  = { cd = 180, class = "MAGE", type = "off", label = "Veines de glace",                 def = true },
    [365350] = { cd = 90,  class = "MAGE", type = "off", label = "Poussée des arcanes",             def = true },
    [321507] = { cd = 45,  class = "MAGE", type = "off", label = "Toucher du magi" },
    [45438]  = { cd = 240, class = "MAGE", type = "def", label = "Bloc de glace",                   def = true },
    [11426]  = { cd = 25,  class = "MAGE", type = "def", label = "Barrière de glace",                def = true }, -- Givre
    [235313] = { cd = 25,  class = "MAGE", type = "def", label = "Barrière ardente",                 def = true }, -- Feu
    [235450] = { cd = 25,  class = "MAGE", type = "def", label = "Barrière prismatique",             def = true }, -- Arcanes
    [110959] = { cd = 120, class = "MAGE", type = "def", label = "Invisibilité supérieure" },
    [235219] = { cd = 300, class = "MAGE", type = "def", label = "Coup de froid" },
    [342245] = { cd = 60,  class = "MAGE", type = "def", label = "Altération temporelle" },
    [80353]  = { cd = 300, class = "MAGE", type = "lust", label = "Distorsion temporelle",          def = true },

    -- ----- Moine -----
    [123904] = { cd = 120, class = "MONK", type = "off", label = "Xuen",                            def = true },
    [137639] = { cd = 90,  class = "MONK", type = "off", label = "Orage, terre et feu",             def = true },
    [392983] = { cd = 40,  class = "MONK", type = "off", label = "Frappe du seigneur du vent" },
    [387184] = { cd = 120, class = "MONK", type = "off", label = "Armes de l'ordre" },
    [122470] = { cd = 90,  class = "MONK", type = "def", label = "Toucher du karma",               def = true },
    [122783] = { cd = 90,  class = "MONK", type = "def", label = "Diffusion de magie" },
    [122278] = { cd = 120, class = "MONK", type = "def", label = "Atténuation du mal" },
    [115203] = { cd = 360, class = "MONK", type = "def", label = "Infusion fortifiante" },
    [116849] = { cd = 120, class = "MONK", type = "ext", label = "Cocon de vie" },
    [115310] = { cd = 180, class = "MONK", type = "ext", label = "Réanimation" },

    -- ----- Paladin -----
    [31884]  = { cd = 120, class = "PALADIN", type = "off", label = "Colère vengeresse",            def = true },
    [343721] = { cd = 60,  class = "PALADIN", type = "off", label = "Jugement final" },
    [255937] = { cd = 30,  class = "PALADIN", type = "off", label = "Sillage de cendres" },
    [375576] = { cd = 60,  class = "PALADIN", type = "off", label = "Cloche divine",                def = true },
    [642]    = { cd = 300, class = "PALADIN", type = "def", label = "Bouclier divin",               def = true },
    [498]    = { cd = 60,  class = "PALADIN", type = "def", label = "Protection divine" },
    [31850]  = { cd = 120, class = "PALADIN", type = "def", label = "Défenseur zélé" },
    [86659]  = { cd = 300, class = "PALADIN", type = "def", label = "Gardien des rois ancestraux" },
    [6940]   = { cd = 120, class = "PALADIN", type = "ext", label = "Bénédiction de sacrifice",     def = true },
    [1022]   = { cd = 300, class = "PALADIN", type = "ext", label = "Bénédiction de protection" },
    [31821]  = { cd = 180, class = "PALADIN", type = "ext", label = "Maîtrise des auras" },
    [633]    = { cd = 600, class = "PALADIN", type = "ext", label = "Imposition des mains" },
    [391054] = { cd = 600, class = "PALADIN", type = "res", label = "Intercession",                 def = true },

    -- ----- Prêtre -----
    [10060]  = { cd = 120, class = "PRIEST", type = "ext", label = "Infusion de puissance",         def = true },
    [34433]  = { cd = 180, class = "PRIEST", type = "off", label = "Servifiel" },
    [228260] = { cd = 120, class = "PRIEST", type = "off", label = "Éruption du Vide",              def = true },
    [391109] = { cd = 60,  class = "PRIEST", type = "off", label = "Ascension ténébreuse" },
    [47585]  = { cd = 120, class = "PRIEST", type = "def", label = "Dispersion",                    def = true },
    [19236]  = { cd = 90,  class = "PRIEST", type = "def", label = "Prière désespérée" },
    [33206]  = { cd = 180, class = "PRIEST", type = "ext", label = "Suppression de la douleur",     def = true },
    [62618]  = { cd = 180, class = "PRIEST", type = "ext", label = "Mot de pouvoir : Barrière",     def = true },
    [47788]  = { cd = 180, class = "PRIEST", type = "ext", label = "Esprit gardien" },
    [64843]  = { cd = 180, class = "PRIEST", type = "ext", label = "Hymne divin" },
    [64901]  = { cd = 180, class = "PRIEST", type = "ext", label = "Symbole d'espoir" },
    [265202] = { cd = 720, class = "PRIEST", type = "ext", label = "Salut (Verbe sacré)" },

    -- ----- Voleur -----
    [13750]  = { cd = 180, class = "ROGUE", type = "off", label = "Poussée d'adrénaline",           def = true },
    [121471] = { cd = 120, class = "ROGUE", type = "off", label = "Lames des ombres",               def = true },
    [185313] = { cd = 60,  class = "ROGUE", type = "off", label = "Danse de l'ombre" },
    [360194] = { cd = 120, class = "ROGUE", type = "off", label = "Marque de mort" },
    [51690]  = { cd = 90,  class = "ROGUE", type = "off", label = "Déferlante d'assassinats" },
    [31224]  = { cd = 120, class = "ROGUE", type = "def", label = "Cape des ombres",                def = true },
    [5277]   = { cd = 120, class = "ROGUE", type = "def", label = "Dérobade" },
    [114018] = { cd = 360, class = "ROGUE", type = "ext", label = "Voile d'occultation" },

    -- ----- Chaman -----
    [2825]   = { cd = 300, class = "SHAMAN", type = "lust", label = "Furie sanguinaire",            def = true },
    [32182]  = { cd = 300, class = "SHAMAN", type = "lust", label = "Héroïsme",                      def = true },
    [114049] = { cd = 180, class = "SHAMAN", type = "off", label = "Ascension",                     def = true },
    [191634] = { cd = 60,  class = "SHAMAN", type = "off", label = "Gardien de la tempête" },
    [198067] = { cd = 150, class = "SHAMAN", type = "off", label = "Élémentaire de feu" },
    [384352] = { cd = 90,  class = "SHAMAN", type = "off", label = "Vents funestes" },
    [51533]  = { cd = 90,  class = "SHAMAN", type = "off", label = "Loups spectraux" },
    [108271] = { cd = 120, class = "SHAMAN", type = "def", label = "Transfert astral",              def = true },
    [98008]  = { cd = 180, class = "SHAMAN", type = "ext", label = "Totem de lien spirituel",       def = true },
    [108280] = { cd = 180, class = "SHAMAN", type = "ext", label = "Totem de marée guérisseuse",    def = true },
    [207399] = { cd = 300, class = "SHAMAN", type = "ext", label = "Totem de protection ancestrale" },

    -- ----- Démoniste -----
    [205180] = { cd = 120, class = "WARLOCK", type = "off", label = "Œil-de-nécromant",             def = true },
    [265187] = { cd = 90,  class = "WARLOCK", type = "off", label = "Tyran démoniaque",             def = true },
    [1122]   = { cd = 180, class = "WARLOCK", type = "off", label = "Invocation d'infernal",        def = true },
    [267217] = { cd = 180, class = "WARLOCK", type = "off", label = "Portail nether" },
    [386997] = { cd = 60,  class = "WARLOCK", type = "off", label = "Putréfaction de l'âme" },
    [104773] = { cd = 180, class = "WARLOCK", type = "def", label = "Résolution sans fin",          def = true },
    [108416] = { cd = 60,  class = "WARLOCK", type = "def", label = "Pacte sombre" },
    [20707]  = { cd = 600, class = "WARLOCK", type = "res", label = "Pierre d'âme",                  def = true },

    -- ----- Guerrier -----
    [1719]   = { cd = 90,  class = "WARRIOR", type = "off", label = "Témérité",                     def = true },
    [107574] = { cd = 90,  class = "WARRIOR", type = "off", label = "Avatar",                        def = true },
    [227847] = { cd = 90,  class = "WARRIOR", type = "off", label = "Tempête de lames" },
    [167105] = { cd = 45,  class = "WARRIOR", type = "off", label = "Broyeur de colosse" },
    [376079] = { cd = 90,  class = "WARRIOR", type = "off", label = "Lance du bastion" },
    [871]    = { cd = 210, class = "WARRIOR", type = "def", label = "Mur de boucliers",             def = true },
    [12975]  = { cd = 180, class = "WARRIOR", type = "def", label = "Baroud d'honneur" },
    [118038] = { cd = 120, class = "WARRIOR", type = "def", label = "Mourir l'arme à la main" },
    [184364] = { cd = 120, class = "WARRIOR", type = "def", label = "Régénération enragée" },
    [97462]  = { cd = 180, class = "WARRIOR", type = "ext", label = "Cri de ralliement",            def = true },

    -- ===== Compléments (audit complet des classes) =====
    -- Chevalier de la mort
    [315443] = { cd = 120, class = "DEATHKNIGHT", type = "off", label = "Membre abominable" },
    [275699] = { cd = 90,  class = "DEATHKNIGHT", type = "off", label = "Apocalypse" },
    [207289] = { cd = 75,  class = "DEATHKNIGHT", type = "off", label = "Assaut impie" },
    [49039]  = { cd = 120, class = "DEATHKNIGHT", type = "def", label = "Lichborne",                 def = true },
    [327574] = { cd = 120, class = "DEATHKNIGHT", type = "def", label = "Pacte sacrificiel",         def = true },
    -- Chasseur de démons
    [196555] = { cd = 120, class = "DEMONHUNTER", type = "def", label = "Marche du Néant",           def = true },
    [212084] = { cd = 60,  class = "DEMONHUNTER", type = "off", label = "Dévastation gangrenée" },
    -- Druide
    [22842]  = { cd = 36,  class = "DRUID", type = "def", label = "Régénération frénétique",         def = true },
    [108238] = { cd = 90,  class = "DRUID", type = "def", label = "Renouveau",                       def = true },
    [319454] = { cd = 300, class = "DRUID", type = "off", label = "Cœur de la nature sauvage",       def = true },
    -- Évocateur
    [370665] = { cd = 60,  class = "EVOKER", type = "ext", label = "Sauvetage",                      def = true },
    [374251] = { cd = 60,  class = "EVOKER", type = "disp", label = "Flamme cautérisante",           def = true }, -- dissipe Saignement/Poison/Malédiction/Maladie
    [370553] = { cd = 120, class = "EVOKER", type = "off", label = "Faire pencher la balance" },
    -- Chasseur
    [193530] = { cd = 120, class = "HUNTER", type = "off", label = "Aspect du sauvage",              def = true },
    [201430] = { cd = 120, class = "HUNTER", type = "off", label = "Ruée" },
    [186289] = { cd = 90,  class = "HUNTER", type = "off", label = "Aspect de l'aigle" },
    -- Mage
    [55342]  = { cd = 120, class = "MAGE", type = "def", label = "Image miroir",                     def = true },
    -- Moine
    [322507] = { cd = 60,  class = "MONK", type = "def", label = "Infusion céleste",                 def = true },
    [115176] = { cd = 300, class = "MONK", type = "def", label = "Méditation zen",                   def = true },
    [325197] = { cd = 180, class = "MONK", type = "ext", label = "Invocation de Chi-Ji",             def = true },
    [152173] = { cd = 90,  class = "MONK", type = "off", label = "Sérénité",                         def = true },
    -- Paladin
    [1044]   = { cd = 25,  class = "PALADIN", type = "ext", label = "Bénédiction de liberté",        def = true },
    [105809] = { cd = 180, class = "PALADIN", type = "off", label = "Justicier zélé",                def = true },
    [231895] = { cd = 120, class = "PALADIN", type = "off", label = "Croisade",                      def = true },
    [184662] = { cd = 90,  class = "PALADIN", type = "def", label = "Bouclier de vengeance",         def = true },
    [205191] = { cd = 60,  class = "PALADIN", type = "def", label = "Œil pour œil" },
    [385438] = { cd = 120, class = "PALADIN", type = "def", label = "Sentinelle",                    def = true },
    [432459] = { cd = 45,  class = "PALADIN", type = "def", label = "Rempart sacré" },   -- Holy Bulwark (Prot, confirmé logs M+)
    [432472] = { cd = 90,  class = "PALADIN", type = "ext", label = "Arme sacrée" },     -- Sacred Weapon (Prot, confirmé logs M+)
    -- Prêtre
    [15286]  = { cd = 120, class = "PRIEST", type = "def", label = "Étreinte vampirique",            def = true },
    [47536]  = { cd = 90,  class = "PRIEST", type = "ext", label = "Extase",                         def = true },
    [246287] = { cd = 90,  class = "PRIEST", type = "ext", label = "Évangélisme",                    def = true },
    [200183] = { cd = 120, class = "PRIEST", type = "off", label = "Apothéose" },
    [73325]  = { cd = 90,  class = "PRIEST", type = "ext", label = "Bond de la foi" },
    -- Voleur
    [1856]   = { cd = 120, class = "ROGUE", type = "def", label = "Disparition",                     def = true },
    [212283] = { cd = 30,  class = "ROGUE", type = "off", label = "Symboles de mort" },
    [381623] = { cd = 60,  class = "ROGUE", type = "def", label = "Thé de chardon",                  def = true },
    -- Chaman
    [108281] = { cd = 120, class = "SHAMAN", type = "def", label = "Conseil ancestral",              def = true },
    [198838] = { cd = 60,  class = "SHAMAN", type = "ext", label = "Totem de mur de terre",          def = true },
    [192249] = { cd = 150, class = "SHAMAN", type = "off", label = "Élémentaire de tempête" },
    [79206]  = { cd = 120, class = "SHAMAN", type = "def", label = "Grâce du marcheur d'esprit" },
    -- Démoniste
    [111898] = { cd = 120, class = "WARLOCK", type = "off", label = "Grimoire : chairon" },
    [113860] = { cd = 120, class = "WARLOCK", type = "off", label = "Âme sombre : misère",           def = true },
    [113858] = { cd = 120, class = "WARLOCK", type = "off", label = "Âme sombre : instabilité",      def = true },
    -- Guerrier
    [23920]  = { cd = 25,  class = "WARRIOR", type = "def", label = "Renvoi de sort",                def = true },
    [228920] = { cd = 90,  class = "WARRIOR", type = "off", label = "Éventreur" },
    [262161] = { cd = 45,  class = "WARRIOR", type = "off", label = "Fauteur de guerre" },

    -- ===== Compléments issus des logs PRO (Midnight 12.0 — spellID confirmés) =====
    -- Certains IDs ont été RÉ-ATTRIBUÉS en Midnight : on ajoute le nouvel ID à
    -- côté de l'ancien (additif, sans risque). Le nom s'affiche via l'API du jeu.
    -- Chevalier de la mort
    [1233448] = { cd = 60,  class = "DEATHKNIGHT", type = "off", label = "Transformation sombre",     def = true }, -- Impie (nouvel ID, ex-63560)
    [48743]   = { cd = 120, class = "DEATHKNIGHT", type = "def", label = "Pacte de mort",             def = true },
    -- Druide
    [102558]  = { cd = 180, class = "DRUID", type = "def", label = "Incarnation : gardien d'Ursoc",   def = true }, -- Gardien
    [204066]  = { cd = 75,  class = "DRUID", type = "def", label = "Rayon lunaire" },
    [102793]  = { cd = 60,  class = "DRUID", type = "ext", label = "Tourbillon d'Ursol" },
    -- Évocateur (Augmentation)
    [442204]  = { cd = 120, class = "EVOKER", type = "off", label = "Souffle des éons",               def = true },
    [406732]  = { cd = 120, class = "EVOKER", type = "ext", label = "Paradoxe spatial" },
    [374968]  = { cd = 120, class = "EVOKER", type = "ext", label = "Spirale temporelle" },
    -- Mage
    [414658]  = { cd = 240, class = "MAGE", type = "def", label = "Froid glacial",                    def = true }, -- variante talent de Bloc de glace
    -- Moine
    [132578]  = { cd = 180, class = "MONK", type = "def", label = "Invocation de Niuzao",             def = true }, -- Brasseur
    [1241059] = { cd = 60,  class = "MONK", type = "def", label = "Infusion céleste",                 def = true }, -- Brasseur (remplace Bière céleste)
    [443028]  = { cd = 90,  class = "MONK", type = "off", label = "Conduit céleste" }, -- héros Conduit
    [116844]  = { cd = 45,  class = "MONK", type = "ext", label = "Cercle de paix" },
    [116841]  = { cd = 30,  class = "MONK", type = "ext", label = "Fougue du tigre" },
    -- Paladin
    [403876]  = { cd = 60,  class = "PALADIN", type = "def", label = "Protection divine",             def = true }, -- ID actuel (ex-498)
    [343527]  = { cd = 60,  class = "PALADIN", type = "off", label = "Sentence d'exécution" },
    [389539]  = { cd = 120, class = "PALADIN", type = "off", label = "Sentinelle" }, -- Prot (talent héros)
    [204018]  = { cd = 180, class = "PALADIN", type = "def", label = "Bénédiction de sauvegarde" },
    [471195]  = { cd = 600, class = "PALADIN", type = "ext", label = "Imposition des mains" }, -- variante Prot
    -- Prêtre
    [472433]  = { cd = 90,  class = "PRIEST", type = "ext", label = "Évangélisme",                    def = true }, -- Disc (nouvel ID)
    [32375]   = { cd = 60,  class = "PRIEST", type = "disp", label = "Dissipation de masse" },       -- dissipe magie en masse
    -- Voleur
    [385627]  = { cd = 60,  class = "ROGUE", type = "off", label = "Fléau du roi",                    def = true }, -- Assassinat
    [185311]  = { cd = 30,  class = "ROGUE", type = "def", label = "Fiole carmin" },
    [57934]   = { cd = 30,  class = "ROGUE", type = "ext", label = "Ruses de corsaire" }, -- Assassinat
    [1224098] = { cd = 30,  class = "ROGUE", type = "ext", label = "Ruses de corsaire" }, -- Hors-la-loi (nouvel ID)
    [381989]  = { cd = 300, class = "ROGUE", type = "off", label = "Ça continue de rouler" }, -- Hors-la-loi
    -- Chaman
    [114050]  = { cd = 180, class = "SHAMAN", type = "off", label = "Ascension" }, -- Élémentaire
    [192077]  = { cd = 120, class = "SHAMAN", type = "ext", label = "Totem de bourrasque" },
    [198103]  = { cd = 300, class = "SHAMAN", type = "def", label = "Élémentaire de terre" },
    [192058]  = { cd = 60,  class = "SHAMAN", type = "ext", label = "Totem capaciteur" },
    -- Démoniste
    [434635]  = { cd = 60,  class = "WARLOCK", type = "off", label = "Ruination",                     def = true }, -- Démono
    [1276452] = { cd = 90,  class = "WARLOCK", type = "off", label = "Grimoire : seigneur diablotin" }, -- Démono
    [6789]    = { cd = 45,  class = "WARLOCK", type = "def", label = "Spirale mortelle" },
    [132411]  = { cd = 30,  class = "WARLOCK", type = "ext", label = "Consumation magique" }, -- dissipe via démon
    [29893]   = { cd = 120, class = "WARLOCK", type = "ext", label = "Puits d'âmes" },
    -- Guerrier
    [436358]  = { cd = 45,  class = "WARRIOR", type = "off", label = "Démolition",                    def = true }, -- héros Colosse
    [3411]    = { cd = 30,  class = "WARRIOR", type = "ext", label = "Intervention" },
    [18499]   = { cd = 60,  class = "WARRIOR", type = "def", label = "Rage berserker" },

    -- ===== Compléments issus des GUIDES METHOD (Midnight — spellID via Wowhead) =====
    -- Défensifs de survie + burst signature + externes, spé par spé. DÉCOCHÉS par
    -- défaut (pas de def=true) pour ne pas surcharger le board : à activer au goût
    -- dans le menu Cooldowns. Le vrai CD est lu en jeu (RealCooldown), la valeur
    -- ci-dessous n'est qu'un filet ; le nom s'affiche dans la langue du client.
    -- Chevalier de la mort
    [1263824] = { cd = 45,  class = "DEATHKNIGHT", type = "off", label = "Consomption" },              -- Sang
    [439843]  = { cd = 60,  class = "DEATHKNIGHT", type = "off", label = "Marque du faucheur" },       -- Deathbringer
    [279302]  = { cd = 90,  class = "DEATHKNIGHT", type = "off", label = "Furie de Sindragosa" },      -- Givre
    [1249658] = { cd = 120, class = "DEATHKNIGHT", type = "off", label = "Souffle de Sindragosa" },    -- Givre
    [455395]  = { cd = 90,  class = "DEATHKNIGHT", type = "off", label = "Abomination levée" },        -- Impie
    -- Chasseur de démons
    [471306]  = { cd = 120, class = "DEMONHUNTER", type = "off", label = "Métamorphose du Vide",       def = true }, -- Dévoreur
    [1241937] = { cd = 60,  class = "DEMONHUNTER", type = "def", label = "Immolation de l'âme",        def = true }, -- Dévoreur
    [1246167] = { cd = 90,  class = "DEMONHUNTER", type = "off", label = "La Traque (Dévoreur)" },
    [162264]  = { cd = 180, class = "DEMONHUNTER", type = "def", label = "Métamorphose (Vengeance)",   def = true },
    [207407]  = { cd = 60,  class = "DEMONHUNTER", type = "off", label = "Sculpteur d'âmes" },         -- Vengeance
    [390163]  = { cd = 60,  class = "DEMONHUNTER", type = "off", label = "Sceau de dépit" },           -- Vengeance
    -- Druide
    [205636]  = { cd = 60,  class = "DRUID", type = "off", label = "Force de la nature" },             -- Équilibre
    [202770]  = { cd = 60,  class = "DRUID", type = "off", label = "Furie d'Élune" },                  -- Équilibre
    [1261867] = { cd = 120, class = "DRUID", type = "off", label = "Cœur de la nature sauvage" },      -- Équi/Farouche/Gardien
    [108294]  = { cd = 120, class = "DRUID", type = "off", label = "Cœur de la nature sauvage" },      -- Restauration
    [274837]  = { cd = 45,  class = "DRUID", type = "off", label = "Frénésie farouche" },              -- Farouche
    [50334]   = { cd = 180, class = "DRUID", type = "off", label = "Berserk (Gardien)",                def = true },
    [117679]  = { cd = 180, class = "DRUID", type = "def", label = "Incarnation : gardien d'Ursoc",    def = true },
    [33891]   = { cd = 180, class = "DRUID", type = "ext", label = "Incarnation : arbre de vie",       def = true }, -- Resto
    [197721]  = { cd = 60,  class = "DRUID", type = "ext", label = "Foisonnement" },                   -- Resto
    [1226140] = { cd = 60,  class = "DRUID", type = "ext", label = "Gardiens du bosquet" },            -- Resto
    [132158]  = { cd = 60,  class = "DRUID", type = "ext", label = "Rapidité de la nature" },          -- Resto
    -- Évocateur
    [403631]  = { cd = 120, class = "EVOKER", type = "off", label = "Souffle des éons" },              -- Aug (variante)
    [360827]  = { cd = 30,  class = "EVOKER", type = "ext", label = "Écailles cloquantes" },           -- Aug
    [404977]  = { cd = 120, class = "EVOKER", type = "off", label = "Saut temporel" },                 -- Aug
    [410352]  = { cd = 60,  class = "EVOKER", type = "def", label = "Étirement du temps" },
    [359816]  = { cd = 120, class = "EVOKER", type = "ext", label = "Vol onirique",                    def = true }, -- Préservation
    [370537]  = { cd = 60,  class = "EVOKER", type = "ext", label = "Stase" },                         -- Préservation
    [429483]  = { cd = 60,  class = "EVOKER", type = "def", label = "Distorsion" },                    -- Préservation
    -- Chasseur
    [53480]   = { cd = 60,  class = "HUNTER", type = "ext", label = "Rugissement du sacrifice",        def = true },
    [1250646] = { cd = 60,  class = "HUNTER", type = "off", label = "Mise à terre" },                  -- Survie
    [1261193] = { cd = 60,  class = "HUNTER", type = "off", label = "Canon-perche" },                  -- Survie
    [1251592] = { cd = 60,  class = "HUNTER", type = "off", label = "Lancer de crocs enflammés" },     -- Survie
    -- Mage
    [414659]  = { cd = 180, class = "MAGE", type = "def", label = "Froid glacial" },                   -- variante ID
    [153561]  = { cd = 45,  class = "MAGE", type = "off", label = "Météore" },                         -- Feu
    [417493]  = { cd = 300, class = "MAGE", type = "def", label = "Coup de froid glacial" },           -- Givre
    -- Moine
    [325153]  = { cd = 60,  class = "MONK", type = "off", label = "Tonneau explosif" },                -- Brasseur
    [322109]  = { cd = 120, class = "MONK", type = "off", label = "Toucher de la mort" },
    [322118]  = { cd = 180, class = "MONK", type = "ext", label = "Invocation de Yu'lon",              def = true }, -- Tisse-brume
    [443059]  = { cd = 90,  class = "MONK", type = "def", label = "Sanctuaire de jade" },              -- Tisse-brume
    [1243287] = { cd = 90,  class = "MONK", type = "def", label = "Diffusion de magie" },              -- Tisse-brume (ID)
    [1249625] = { cd = 120, class = "MONK", type = "off", label = "Zénith" },                          -- Marche-vent
    -- Paladin
    [384376]  = { cd = 120, class = "PALADIN", type = "off", label = "Colère vengeresse (Sacré)",     def = true },
    -- Prêtre
    [421453]  = { cd = 240, class = "PRIEST", type = "off", label = "Pénitence ultime",                def = true }, -- Disc
    [586]     = { cd = 30,  class = "PRIEST", type = "def", label = "Disparition" },
    [120517]  = { cd = 60,  class = "PRIEST", type = "ext", label = "Halo" },                          -- Sacré
    [120644]  = { cd = 60,  class = "PRIEST", type = "off", label = "Halo (Ombre)" },                  -- Archon
    -- Voleur
    [280719]  = { cd = 45,  class = "ROGUE", type = "off", label = "Technique secrète" },              -- Finesse
    -- Chaman
    [443454]  = { cd = 60,  class = "SHAMAN", type = "off", label = "Rapidité ancestrale" },           -- Élém (Farseer)
    [114051]  = { cd = 120, class = "SHAMAN", type = "off", label = "Ascension (Amélioration)",        def = true },
    [197214]  = { cd = 40,  class = "SHAMAN", type = "off", label = "Fracture" },                      -- Amélioration
    [444995]  = { cd = 60,  class = "SHAMAN", type = "off", label = "Totem de déferlement" },          -- Amélioration
    [383013]  = { cd = 120, class = "SHAMAN", type = "ext", label = "Totem de purification du poison" }, -- Resto
    -- Démoniste
    [442726]  = { cd = 60,  class = "WARLOCK", type = "off", label = "Malveillance",                   def = true }, -- Affliction
    [430014]  = { cd = 60,  class = "WARLOCK", type = "off", label = "Malveillance",                   def = true }, -- Destruction
    -- Guerrier
    [202168]  = { cd = 25,  class = "WARRIOR", type = "def", label = "Victoire imminente" },
    [1244088] = { cd = 30,  class = "WARRIOR", type = "ext", label = "S'interposer" },
    [385059]  = { cd = 45,  class = "WARRIOR", type = "off", label = "Furie d'Odyn" },                 -- Fureur
    [385952]  = { cd = 45,  class = "WARRIOR", type = "off", label = "Charge de bouclier" },           -- Protection
    [1160]    = { cd = 45,  class = "WARRIOR", type = "def", label = "Cri démoralisant",               def = true }, -- Protection

    -- ===== Compléments 2e vague de logs PRO (Midnight 12.0 — castés, confirmés) =====
    [392060]  = { cd = 60,  class = "HUNTER", type = "off", label = "Flèche gémissante" },           -- Rôdeur noir
    [19801]   = { cd = 10,  class = "HUNTER", type = "disp", label = "Tir tranquillisant" },          -- dissipe enrage/magie (offensif)
    [108199]  = { cd = 120, class = "DEATHKNIGHT", type = "ext", label = "Poigne de Gorefiend" },    -- Sang (regroupement)
    [1265384] = { cd = 120, class = "DEATHKNIGHT", type = "off", label = "Furie de Sindragosa" },    -- Givre (variante héros)
    [385954]  = { cd = 45,  class = "WARRIOR", type = "off", label = "Charge de bouclier" },          -- Prot (variante ID)
    [385060]  = { cd = 45,  class = "WARRIOR", type = "off", label = "Furie d'Odyn" },                -- Fureur (variante ID)
    [446035]  = { cd = 90,  class = "WARRIOR", type = "off", label = "Tempête de lames (Fureur)" },
    [1234195] = { cd = 60,  class = "DEMONHUNTER", type = "off", label = "Nova du Vide" },            -- Dévoreur
    [1253593] = { cd = 60,  class = "PRIEST", type = "def", label = "Bouclier du Vide" },            -- Tisse-vide
    [1257052] = { cd = 60,  class = "WARLOCK", type = "off", label = "Moisson noire" },              -- Affliction
    [469270]  = { cd = 60,  class = "SHAMAN", type = "off", label = "Vents funestes" },              -- Amélioration (variante ID)
    [1218090] = { cd = 60,  class = "SHAMAN", type = "off", label = "Tempête primordiale" },         -- Amélioration (héros)
    [1253799] = { cd = 60,  class = "DRUID", type = "off", label = "Rugissement fracassant" },       -- Gardien (héros)
    [443591]  = { cd = 90,  class = "MONK", type = "off", label = "Unité intérieure" },              -- Tisse-brume (héros)
    [263165]  = { cd = 45,  class = "PRIEST", type = "off", label = "Torrent du Vide" },             -- Ombre
    [1267068] = { cd = 60,  class = "SHAMAN", type = "off", label = "Totem de flux-tempête" },       -- Amélioration (héros)

    -- ---------- Dispels (retrait de debuffs alliés / dissipation) ----------
    -- CD de base 8 s pour les dispels simple-cible (le vrai CD/talents est lu à
    -- l'exécution). Sur le board ils sont placés JUSTE AVANT la potion. La spé est
    -- filtrée par les CD réellement connus (message CK). NB : Dissipation de masse
    -- (32375), Tir tranquillisant (19801) et Flamme cautérisante (374251) sont déjà
    -- suivis dans d'autres catégories (ext/def) — non redéfinis ici.
    [527]    = { cd = 8,  class = "PRIEST",      type = "disp", label = "Purification" },              -- Purify (Magie/Maladie)
    [4987]   = { cd = 8,  class = "PALADIN",     type = "disp", label = "Purification" },              -- Cleanse (Holy)
    [213644] = { cd = 8,  class = "PALADIN",     type = "disp", label = "Purification des toxines" }, -- Cleanse Toxins
    [88423]  = { cd = 8,  class = "DRUID",       type = "disp", label = "Traitement naturel" },        -- Nature's Cure (Resto)
    [2782]   = { cd = 8,  class = "DRUID",       type = "disp", label = "Suppression de la corruption" }, -- Remove Corruption
    [77130]  = { cd = 8,  class = "SHAMAN",      type = "disp", label = "Purification de l'esprit" },  -- Purify Spirit (Resto)
    [51886]  = { cd = 8,  class = "SHAMAN",      type = "disp", label = "Purification de l'esprit" },  -- Cleanse Spirit
    [475]    = { cd = 8,  class = "MAGE",        type = "disp", label = "Suppression de la malédiction" }, -- Remove Curse
    [115450] = { cd = 8,  class = "MONK",        type = "disp", label = "Détoxification" },            -- Detox (Mistweaver)
    [218164] = { cd = 8,  class = "MONK",        type = "disp", label = "Détoxification" },            -- Detox (non-Tisse-brume)
    [360823] = { cd = 8,  class = "EVOKER",      type = "disp", label = "Naturalisation" },            -- Naturalize (Préservation)
    [365585] = { cd = 8,  class = "EVOKER",      type = "disp", label = "Expurgation" },               -- Expunge (Poison)
    [19505]  = { cd = 15, class = "WARLOCK",     type = "disp", label = "Dévorer la magie" },          -- Devour Magic (chasseur corrompu)
    [278326] = { cd = 10, class = "DEMONHUNTER", type = "disp", label = "Consumer la magie" },         -- Consume Magic (offensif)
}

-- Ordre & libellés des catégories de CD (pour l'UI)
NS.CD_TYPES = {
    { key = "off",  label = "Offensifs" },
    { key = "def",  label = "Défensifs" },
    { key = "ext",  label = "Externes / raid" },
    { key = "lust", label = "Bloodlust" },
    { key = "res",  label = "Résurrection" },
    { key = "disp", label = "Dissipations" },
    { key = "pot",  label = "Potions" },
}

-- Potions de combat suivies (usage partagé dans le board CD). itemID(s) par
-- potion — le sort d'effet est résolu à l'exécution via C_Item.GetItemSpell.
NS.POTION_ITEMS = {
    -- toutes les qualités (bronze/argent/or) + versions « fugaces » : le sort
    -- d'effet est résolu à l'exécution par GetItemSpell, donc n'importe quel
    -- rang compte. IDs fournis (Wowhead, Midnight 12.0).
    { ids = { 241308, 241309, 245897, 245898 },                 label = "Potentiel de la Lumière" },   -- Light's Potential
    { ids = { 241288, 241289, 245902, 245903 },                 label = "Potion de témérité" },          -- Potion of Recklessness
    { ids = { 241292, 245910 },                                 label = "Breuvage d'abandon effréné" },  -- Draught of Rampant Abandon
    { ids = { 212263, 212264, 212265, 212969, 212970, 212971 }, label = "Potion trempée" },              -- Tempered Potion
}
-- Détection DIRECTE par spellID d'effet (repli quand l'objet n'est pas encore
-- en cache, et pour les potions dont on n'a pas l'itemID). IDs relevés dans les
-- logs M+ Midnight. Sert au board CD ET au compteur « Pot » des stats.
--   type "burst" = potion offensive/utilitaire ; type "hp" = potion/soin de vie.
NS.POTION_EFFECTS = {
    [1236616] = { label = "Potentiel de la Lumière",    kind = "burst" }, -- Light's Potential (log M+)
    [1236994] = { label = "Potion de témérité",         kind = "burst" }, -- Potion of Recklessness (log M+)
    [1236998] = { label = "Breuvage d'abandon effréné", kind = "burst" }, -- Draught of Rampant Abandon (log M+)
    [6262]    = { label = "Pierre de soins",            kind = "hp" },    -- Healthstone (ID historique stable)
}
-- Repli par NOM pour les potions de SOIN (« flacon de HP ») : on ne connaît
-- pas encore leur spellID d'effet Midnight, mais leur nom est stable. Toute
-- action dont le nom du sort contient un de ces fragments compte comme potion.
-- fragments SPÉCIFIQUES aux consommables (évite tout faux positif avec un sort
-- de classe) : chacun contient « potion »/« soin »/« stone ».
NS.POTION_NAME_HINTS = { "potion de soin", "healing potion", "potion de vie",
    "health potion", "pierre de soin", "healthstone" }
NS.CLASS_ORDER = {
    "DEATHKNIGHT", "DEMONHUNTER", "DRUID", "EVOKER", "HUNTER", "MAGE",
    "MONK", "PALADIN", "PRIEST", "ROGUE", "SHAMAN", "WARLOCK", "WARRIOR",
}

-- Un CD est-il affiché ? (choix perso stocké dans db.cdShow, sinon défaut)
function NS.IsCDShown(spellID)
    local info = NS.COOLDOWNS[spellID]
    if not info then return false end
    local ov = NS.db and NS.db.cdShow and NS.db.cdShow[spellID]
    if ov ~= nil then return ov end
    return info.def and true or false
end

-- Spé(s) d'un CD : specID unique, liste de specID, ou ABSENT = toute la classe.
-- Sert à ne montrer que les CD réellement dispos pour la spé du joueur
-- (un mage Feu ne verra plus les CD Givre/Arcanes). specID : voir SPEC_TOKENS.
NS.CD_SPEC = {
    -- Chevalier de la mort (250 Sang · 251 Givre · 252 Impie)
    [51271] = 251, [47568] = 251,
    [49028] = 250, [55233] = 250,
    [42650] = 252,
    -- Chasseur de démons (577 Dévastation · 581 Vengeance)
    [191427] = 577, [258925] = 577, [258860] = 577, [198589] = 577,
    [187827] = 581, [204021] = 581,
    -- Druide (102 Équilibre · 103 Farouche · 104 Gardien · 105 Restauration)
    [194223] = 102, [102560] = 102,
    [106951] = 103,
    [61336] = { 103, 104 }, [200851] = 104,
    [740] = 105, [102342] = 105, [29166] = 105,
    -- Évocateur (1467 Dévastation · 1468 Préservation · 1473 Augmentation)
    [375087] = 1467, [357210] = 1467,
    [363534] = 1468, [370960] = 1468, [357170] = 1468,
    -- Chasseur (253 Maîtrise des bêtes · 254 Précision · 255 Survie)
    [19574] = 253, [359844] = 253,
    [288613] = 254,
    [266779] = 255, [360966] = 255,
    -- Mage (62 Arcanes · 63 Feu · 64 Givre)
    [190319] = 63, [235313] = 63,
    [12472] = 64, [11426] = 64,
    [365350] = 62, [321507] = 62, [110959] = 62, [235450] = 62,
    -- Moine (268 Maître brasseur · 269 Marche-vent · 270 Tisse-brume)
    [123904] = 269, [137639] = 269, [392983] = 269, [122470] = 269,
    [387184] = 268,
    [116849] = 270, [115310] = 270,
    -- Paladin (65 Sacré · 66 Protection · 70 Vindicte)
    [343721] = 70, [255937] = 70,
    [31850] = 66, [86659] = 66,
    [31821] = 65,
    -- Prêtre (256 Discipline · 257 Sacré · 258 Ombre)
    [228260] = 258, [391109] = 258, [47585] = 258,
    [33206] = 256, [62618] = 256,
    [47788] = 257, [64843] = 257, [64901] = 257, [265202] = 257,
    -- Voleur (259 Assassinat · 260 Hors-la-loi · 261 Finesse)
    [360194] = 259,
    [13750] = 260, [51690] = 260,
    [121471] = 261, [185313] = 261,
    -- Chaman (262 Élémentaire · 263 Amélioration · 264 Restauration)
    [191634] = 262, [198067] = 262,
    [384352] = 263, [51533] = 263,
    [98008] = 264, [108280] = 264, [207399] = 264,
    -- Démoniste (265 Affliction · 266 Démonologie · 267 Destruction)
    [205180] = 265,
    [265187] = 266, [267217] = 266,
    [1122] = 267,
    -- Guerrier (71 Armes · 72 Fureur · 73 Protection)
    [1719] = 72, [385059] = 72, [184364] = 72,
    [167105] = 71, [118038] = 71,
    [227847] = { 71, 72 },
    [871] = 73,
}

-- La spé donnée peut-elle utiliser ce CD ? (spé inconnue → on montre tout)
function NS.CDMatchesSpec(spellID, specID)
    local sp = NS.CD_SPEC[spellID]
    if sp == nil then return true end -- CD de toute la classe
    if not specID or specID == 0 then return true end -- spé inconnue
    if type(sp) == "number" then return sp == specID end
    if type(sp) == "table" then
        for _, s in ipairs(sp) do if s == specID then return true end end
        return false
    end
    return true
end

-- ============================================================
-- Utilitaires partagés (une seule copie, utilisée par tous les
-- modules — avant : dupliqués dans Kicks/Cooldowns/Loot/Vault/UI).
-- ============================================================

-- Midnight : certaines valeurs sont « secrètes » → toute comparaison plante.
function NS.SafeNum(v)
    if issecretvalue and issecretvalue(v) then return nil end
    return v
end

-- pet → propriétaire ("pet"→"player", "partypetN"→"partyN", "raidpetN"→"raidN")
function NS.OwnerUnit(unit)
    if unit == "pet" then return "player" end
    local p = unit:match("^partypet(%d+)$")
    if p then return "party" .. p end
    local r = unit:match("^raidpet(%d+)$")
    if r then return "raid" .. r end
    return unit
end

-- Clé « Nom-Royaume » d'une unité (même format que les clés de peers)
function NS.UnitKey(unit)
    local name, realm = UnitFullName(unit)
    if not name then return nil end
    realm = (realm and realm ~= "" and realm)
        or (GetNormalizedRealmName and GetNormalizedRealmName()) or ""
    return (realm ~= "") and (name .. "-" .. realm) or name
end

-- ---------- Contexte de contenu + groupe réel (0.28) ----------
-- Contexte courant : "world" (open world / solo), "dungeon" (donjon M+/normal,
-- scénario), "raid". Sert aux toggles par-contenu des modules kick/cooldown.
function NS.ContentContext()
    local _, itype = IsInInstance()
    if itype == "raid" then return "raid" end
    if itype == "party" or itype == "scenario" then return "dungeon" end
    return "world"
end

-- Le module (prefix "kick" ou "cd") est-il activé dans le CONTEXTE courant ?
-- Toggles db : <prefix>World / <prefix>Dungeon / <prefix>Raid (défaut = true).
function NS.ModuleEnabledHere(prefix)
    if not NS.db then return true end
    local ctx = NS.ContentContext()
    local key = prefix .. ((ctx == "raid" and "Raid") or (ctx == "dungeon" and "Dungeon") or "World")
    return NS.db[key] ~= false
end

-- En groupe avec au moins un AUTRE VRAI JOUEUR ? (un groupe uniquement composé
-- de PNJ — donjon en suiveurs, Brann des gouffres… — ne compte pas → modules
-- non activés). Les kicks/CD n'ont de sens qu'avec de vrais coéquipiers.
function NS.HasRealGroup()
    if IsInRaid() then
        for i = 1, 40 do
            local u = "raid" .. i
            if UnitExists(u) and not UnitIsUnit(u, "player") and UnitIsPlayer(u) then return true end
        end
    elseif IsInGroup() then
        for i = 1, 4 do
            local u = "party" .. i
            if UnitExists(u) and UnitIsPlayer(u) then return true end
        end
    end
    return false
end

-- Unités des coéquipiers à suivre (hors soi). EN RAID : uniquement le
-- SOUS-GROUPE du joueur (les 5 de notre groupe), jamais tout le raid.
function NS.MyGroupUnits()
    local units = {}
    if IsInRaid() then
        local mySub
        for i = 1, 40 do
            if UnitExists("raid" .. i) and UnitIsUnit("raid" .. i, "player") then
                mySub = select(3, GetRaidRosterInfo(i)); break
            end
        end
        for i = 1, 40 do
            local u = "raid" .. i
            if UnitExists(u) and not UnitIsUnit(u, "player")
                and select(3, GetRaidRosterInfo(i)) == mySub then
                units[#units + 1] = u
            end
        end
    elseif IsInGroup() then
        for i = 1, 4 do
            if UnitExists("party" .. i) then units[#units + 1] = "party" .. i end
        end
    end
    return units
end

-- Cooldown RÉEL d'un sort pour le joueur local — talents, charges et hâte
-- appliqués, lu directement dans le jeu. Chaque client lit SON vrai CD
-- (au moment du cast) et le diffuse → tout le monde a le vrai cooldown de
-- tout le monde. La valeur de base (table) n'est plus qu'un filet de secours.
local SafeNum = NS.SafeNum
function NS.RealCooldown(spellID, baseCD)
    if not spellID then return baseCD end
    -- sorts à charges : durée de recharge d'une charge
    local cfn = C_Spell and C_Spell.GetSpellCharges
    if cfn then
        local ok, ci = pcall(cfn, spellID)
        if ok and type(ci) == "table" then
            local mx   = SafeNum(ci.maxCharges)
            local cdur = SafeNum(ci.cooldownDuration)
            if mx and mx > 1 and cdur and cdur > 1.5 then return cdur end
        end
    elseif GetSpellCharges then
        local ok, _, max, _, dur = pcall(GetSpellCharges, spellID)
        max, dur = SafeNum(max), SafeNum(dur)
        if ok and max and max > 1 and dur and dur > 1.5 then return dur end
    end
    -- cooldown normal
    local dur
    local gfn = C_Spell and C_Spell.GetSpellCooldown
    if gfn then
        local ok, info = pcall(gfn, spellID)
        if ok and type(info) == "table" then dur = SafeNum(info.duration) end
    elseif GetSpellCooldown then
        local ok, _, d = pcall(GetSpellCooldown, spellID)
        if ok then dur = SafeNum(d) end
    end
    -- on écarte le GCD (~1,5 s) : on ne garde que si c'est un vrai CD
    if dur and dur > 1.5 then return dur end
    return baseCD
end

local atan2 = math.atan2 or function(y, x) return math.atan(y, x) end

local DEFAULTS = {
    minimapPos  = 212,  -- angle (degrés) du bouton autour de la minimap
    kickOverlay = true, -- encadré des kicks visible dans l'interface
    kickBgAlpha = 0.6,  -- opacité du fond de l'encadré des kicks
    kickLocked  = false, -- verrou de taille (masque la poignée de redimensionnement)
    kickRotation = true, -- rotation de kick : cadre vert (prochain) / orange (suivant)
    -- Activation des modules kick/cooldown PAR CONTEXTE (0.28). Les 3 cochés par
    -- défaut ; les overlays n'apparaissent qu'en groupe avec un vrai joueur.
    kickWorld   = true, kickDungeon = true, kickRaid = true,
    cdWorld     = true, cdDungeon   = true, cdRaid   = true,
    alertChat   = true, -- alerte chat quand une ressource tombe à 0
    alertRaid   = true, -- alerte au centre de l'écran (style raid warning)
    alertSound  = true, -- alerte sonore
    durAlert    = true, -- alerte quand la durabilité de l'équipement est basse
    durYellow   = 25,   -- seuil jaune (%) — alerte moyenne
    durRed      = 10,   -- seuil rouge (%) — alerte critique
    -- Alerte Bloodlust (Potion + Burst) + stats M+
    blAlert     = true, -- message + son quand un Lust est lancé (Potion + Burst)
    blAlertSound = true, -- jouer un son avec l'alerte BL
    blAlertSize = 30,   -- taille du texte de l'alerte BL
    blAlertR    = 1,    -- couleur alerte BL (rouge)
    blAlertG    = 0.82, -- couleur alerte BL (vert)
    blAlertB    = 0,    -- couleur alerte BL (bleu)
    blAlertColorIdx = 1, -- préréglage de couleur choisi dans le menu (1=jaune)
    statsPopup  = true, -- afficher le popup de stats à la fin d'un donjon M+
    statsPopupScale = 1, -- échelle mémorisée du popup de fin (redimensionnable)
    lootTooltip = false, -- aide au butin : upgrades dans les infobulles (off par défaut)
    cdOverlay   = true, -- encadré du tracker de cooldowns visible
    cdBgAlpha   = 0.6,  -- opacité du fond de l'encadré des CD
    cdShowCat   = true, -- afficher la catégorie (Off/Déf/Ext/BL/Rez/Pot) dans le tooltip
    cdTrackPot  = true, -- suivre l'usage des potions de combat dans le board
    cdRowGap    = 0,    -- espace (px) entre chaque ligne de membre (0 = collé)
    cdNameW     = 92,   -- largeur de la colonne des noms (px) : + petit = icônes collées au pseudo
    cdLocked    = false, -- verrou de taille (masque la poignée de redimensionnement)
    appelReport = true, -- rapport chat des manques à l'appel (affichage local)
    spellIDTooltip = true, -- SpellID/IconID dans les infobulles (Toolbox)
    keyAutoSlot = true, -- insérer sa clé auto quand le réceptacle s'ouvre
    keyAssistant = true, -- boutons Ready check + Pull sur la fenêtre de clé
    -- Toolbox (automatisations de confort)
    tbAutoSell   = true,  -- vendre les objets gris auto chez un marchand
    tbAutoRepair = true,  -- réparer auto
    tbRepairGuild = false, -- réparation : fonds de guilde en priorité (sinon ton or)
    tbAutoQueue  = true,  -- accepter auto les files (donjon / rôle / BG)
    tbAutoLogDungeon = false, -- journal de combat auto en donjon (Warcraft Logs)
    tbAutoLogRaid    = false, -- journal de combat auto en raid (Warcraft Logs)
}

-- ---------- Buffs & auras suivis (tables éditables) ----------
-- Astuce : /asylum auras liste les spellID de tes buffs actuels,
-- pratique pour compléter ces tables.

-- Auras de flacons (Midnight — spellID de l'aura, pas de l'objet)
NS.FLASK_AURAS = {
    [1230878] = true, -- Flask of the Shattered Sun
    [1230876] = true, -- Flask of the Magisters
    [1230877] = true, -- Flask of the Blood Knights
    [1235108] = true, -- Flask of the Magisters (variante de qualité — vérifié wowhead)
    [1235109] = true, -- variante de qualité (déduite de la séquence, ID mort = inoffensif)
    [1235110] = true, -- variante de qualité
    [1235111] = true, -- variante de qualité
}

-- Auras « Bien nourri » (Midnight). NB : l'API renvoie plusieurs spellID
-- distincts tous nommés « Well Fed » selon la stat du festin — Buffs.lua a
-- aussi un repli par NOM (« Bien nourri »/« Well Fed ») pour tout attraper.
NS.FOOD_AURAS = {
    [1278929] = true, -- Hearty Well Fed (festins Hearty)
    [461957]  = true, -- Well Fed (générique récent)
    [1232585] = true, -- Well Fed (Midnight : 98 endu + 50 stat primaire — vérifié wowhead)
    [1284616] = true, -- Well Fed (Midnight : critique — vérifié wowhead)
    [1232490] = true, -- Well Fed (Midnight : autre stat — vu dans les logs M+)
}
-- Repli par NOM (indépendant des spellID, qui changent à chaque festin) :
-- toute aura dont le nom contient un de ces fragments compte comme bouffe/flacon.
NS.FOOD_NAME_HINTS  = { "bien nourri", "well fed", "rassasié" }
NS.FLASK_NAME_HINTS = { "flacon", "flask", "fiole", "phial" }

-- Qualité OR (0.28) : le check ne passe VERT que pour la meilleure qualité.
--  · Flacon : les qualités (bronze/argent/or) partagent le MÊME spellID d'aura,
--    seule la VALEUR change → on lit la valeur de l'aura (OR = 165 cette saison).
--  · Bouffe : l'aura « copieux » (Hearty) 1219179 = top ; toute autre « Bien
--    nourri » = qualité inférieure.
--  · Huile : enchant d'arme temporaire distinct par qualité (OR = 8052).
-- Une qualité inférieure = rond orange ; rien = croix rouge ; OR = coche verte.
NS.FLASK_GOLD_VALUE  = 165
NS.FOOD_GOLD_AURAS   = { [1219179] = true }
NS.OIL_GOLD_ENCHANTS = { [8052] = true }

-- Buffs de groupe par classe : colonne affichée si la classe est
-- présente dans le groupe
NS.RAID_BUFFS = {
    { class = "PRIEST",  label = "Robu.",   ids = { [21562]  = true } }, -- Mot de pouvoir : Robustesse
    { class = "MAGE",    label = "Int.",    ids = { [1459]   = true } }, -- Intelligence des arcanes
    { class = "DRUID",   label = "Sauvage", ids = { [1126]   = true } }, -- Marque du fauve
    { class = "WARRIOR", label = "Cri",     ids = { [6673]   = true } }, -- Cri de guerre
    { class = "EVOKER",  label = "Bronze",  ids = { [381748] = true } }, -- Bénédiction du bronze
    { class = "SHAMAN",  label = "Furie",   ids = { [462854] = true } }, -- Furie-des-cieux
    { class = "PALADIN", label = "Aura",    ids = { [465] = true, [32223] = true, [317920] = true } }, -- Auras
}

-- Emplacements vérifiés pour le check d'enchantement (8 slots).
-- En Midnight : TÊTE et ÉPAULES sont enchantables ; pas de cape ni poignets.
-- Éditable : ordre = ordre d'affichage. inv : 1 tête · 3 épaules · 5 torse
-- · 16 arme · 7 jambes · 8 pieds · 11/12 anneaux.
NS.ENCHANT_SLOTS = {
    { inv = 1,  label = "Tête" },
    { inv = 3,  label = "Épaules" },
    { inv = 5,  label = "Torse" },
    { inv = 16, label = "Arme" },
    { inv = 7,  label = "Jambes" },
    { inv = 8,  label = "Pieds" },
    { inv = 11, label = "Anneau 1" },
    { inv = 12, label = "Anneau 2" },
}

-- Note : le « buff d'arme propre » (Rite de sanctification du paladin,
-- sort 433568, talent Lightsmith) pose un enchantement d'arme temporaire
-- (comme un poison de voleur) → il est détecté par GetWeaponEnchantInfo,
-- donc la colonne « Huile » du check de prépa le compte automatiquement :
-- pala avec son Rite = vert, sans rien = rouge. Rien de spécial à faire.

-- Alerte quand un consommable suivi tombe à 0 (une fois par session/objet)
-- Chaque canal (chat / écran / son) est activable dans le menu Alertes.
local depletedNotified = {}
function NS.NotifyDepleted(itemID)
    if depletedNotified[itemID] then return end
    depletedNotified[itemID] = true
    local db = NS.db or {}
    if db.alertChat == false and db.alertRaid == false and db.alertSound == false then return end
    local function announce(name)
        local label = name or ("objet " .. itemID)
        if db.alertChat ~= false then
            NS.Msg((NS.L("|cffea4850stock épuisé :|r %s")):format(label))
        end
        if db.alertRaid ~= false and RaidNotice_AddMessage and RaidWarningFrame then
            pcall(RaidNotice_AddMessage, RaidWarningFrame,
                (NS.L("Stock épuisé : %s")):format(label),
                (ChatTypeInfo and ChatTypeInfo["RAID_WARNING"]) or { r = 1, g = 0.28, b = 0.31 })
        end
        if db.alertSound ~= false and SOUNDKIT and PlaySound then
            PlaySound(SOUNDKIT.RAID_WARNING)
        end
    end
    if Item and Item.CreateFromItemID then
        local it = Item:CreateFromItemID(itemID)
        it:ContinueOnItemLoad(function() announce(it:GetItemName()) end)
    else
        announce(nil)
    end
end

-- Avertissement quand un consommable passe sous le seuil « stock bas »
-- (une fois par objet/session, mêmes canaux d'alerte que l'épuisement).
local lowNotified = {}
function NS.NotifyLow(itemID, count)
    if lowNotified[itemID] then return end
    lowNotified[itemID] = true
    local db = NS.db or {}
    if db.alertChat == false and db.alertRaid == false and db.alertSound == false then return end
    local function announce(name)
        local label = name or ("objet " .. itemID)
        if db.alertChat ~= false then
            NS.Msg((NS.L("|cfffacd40stock bas :|r %s (%d)")):format(label, count or 0))
        end
        if db.alertRaid ~= false and RaidNotice_AddMessage and RaidWarningFrame then
            pcall(RaidNotice_AddMessage, RaidWarningFrame,
                (NS.L("Stock bas : %s (%d)")):format(label, count or 0),
                { r = 0.98, g = 0.80, b = 0.25 })
        end
        if db.alertSound ~= false and SOUNDKIT and PlaySound then
            PlaySound(SOUNDKIT.IG_QUEST_LOG_OPEN or SOUNDKIT.RAID_WARNING)
        end
    end
    if Item and Item.CreateFromItemID then
        local it = Item:CreateFromItemID(itemID)
        it:ContinueOnItemLoad(function() announce(it:GetItemName()) end)
    else
        announce(nil)
    end
end

-- Ré-arme les alertes d'un objet dès qu'il repasse au-dessus du seuil (réappro)
-- → on peut être ré-alerté si on le laisse retomber plus tard dans la session.
function NS.RearmStock(itemID, count)
    local low = NS.LOW_STOCK or 5
    if (count or 0) > low then lowNotified[itemID] = nil end
    if (count or 0) > 0 then depletedNotified[itemID] = nil end
end

-- Vérif PROACTIVE : alerte tout de suite les consommables déjà bas/épuisés,
-- SANS attendre une transition. Appelée à l'entrée en donjon/raid (le bon
-- moment pour savoir si on est prêt) — avant, si on arrivait DÉJÀ bas, aucune
-- alerte ne partait (elle ne se déclenchait que sur un passage sous le seuil).
-- Ne concerne que les objets que ce perso possède habituellement (itemsSeen).
function NS.WarnLowConsumablesNow()
    if not NS.db then return end
    local low = NS.LOW_STOCK or 5
    for _, t in ipairs(NS.TRACKED_ITEMS or {}) do
        local pid = t.ids and t.ids[1]
        local seen = NS.chardb and NS.chardb.itemsSeen and NS.chardb.itemsSeen[pid]
        if pid and seen and NS.CountTracked then
            local c = NS.CountTracked(t)
            if c == 0 then NS.NotifyDepleted(pid)
            elseif c <= low then NS.NotifyLow(pid, c) end
        end
    end
end

-- ---------- Alerte de durabilité de l'équipement ----------
-- Durabilité globale = somme(actuel) / somme(max) des pièces équipées (%).
-- Jaune sous le seuil moyen, rouge sous le seuil critique (personnalisables).
-- On n'alerte qu'à l'AGGRAVATION (un seul avertissement par palier franchi) ;
-- se réparer réarme l'alerte. Mêmes canaux que les conso (chat/écran/son).
local function DurabilityPct()
    if not GetInventoryItemDurability then return nil end
    local cur, max = 0, 0
    for slot = 1, 18 do
        local c, m = GetInventoryItemDurability(slot)
        if c and m and m > 0 then cur = cur + c; max = max + m end
    end
    if max == 0 then return nil end
    return cur / max * 100
end

local DUR_ORDER = { ok = 0, yellow = 1, red = 2 }
local durState = "ok"
function NS.CheckDurability()
    local db = NS.db or {}
    if db.durAlert == false then durState = "ok"; return end
    local pct = DurabilityPct()
    if not pct then return end
    local red    = db.durRed or 10
    local yellow = db.durYellow or 25
    local newState = (pct < red) and "red" or (pct < yellow) and "yellow" or "ok"
    local worse = (DUR_ORDER[newState] or 0) > (DUR_ORDER[durState] or 0)
    durState = newState
    if not worse then return end -- amélioration/stable : pas de nouvelle alerte
    if db.alertChat == false and db.alertRaid == false and db.alertSound == false then return end
    local p = math.floor(pct)
    local chatFmt, screen, col
    if newState == "red" then
        chatFmt = NS.L("|cffea4850Durabilité critique :|r %d%%")
        screen  = (NS.L("Durabilité critique : %d%%")):format(p)
        col = { 0.94, 0.28, 0.31 }
    else
        chatFmt = NS.L("|cfffacd40Durabilité faible :|r %d%%")
        screen  = (NS.L("Durabilité faible : %d%%")):format(p)
        col = { 0.98, 0.80, 0.25 }
    end
    if db.alertChat ~= false then NS.Msg(chatFmt:format(p)) end
    if db.alertRaid ~= false and RaidNotice_AddMessage and RaidWarningFrame then
        pcall(RaidNotice_AddMessage, RaidWarningFrame, screen, col)
    end
    if db.alertSound ~= false and SOUNDKIT and PlaySound then
        PlaySound(SOUNDKIT.RAID_WARNING)
    end
end

local durFrame = CreateFrame("Frame")
durFrame:RegisterEvent("UPDATE_INVENTORY_DURABILITY")
durFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
durFrame:RegisterEvent("PLAYER_REGEN_ENABLED")   -- fin de combat → re-check réactif
local durPending
durFrame:SetScript("OnEvent", function(_, event)
    -- Entrée en donjon/raid : ré-arme les alertes conso et fait une PRÉPA fraîche
    -- (durabilité + stocks déjà bas) AU BON MOMENT. Petit délai : sacs/durabilité
    -- pas encore chargés à la seconde exacte de l'entrée.
    if event == "PLAYER_ENTERING_WORLD" and IsInInstance then
        local inInst, itype = IsInInstance()
        if inInst and (itype == "party" or itype == "raid" or itype == "scenario") then
            wipe(lowNotified); wipe(depletedNotified); durState = "ok"
            if C_Timer and C_Timer.After then
                C_Timer.After(3, function()
                    pcall(NS.CheckDurability)
                    if NS.WarnLowConsumablesNow then pcall(NS.WarnLowConsumablesNow) end
                end)
            end
            return
        end
    end
    if durPending then return end
    durPending = true
    C_Timer.After(1, function() durPending = nil; pcall(NS.CheckDurability) end)
end)

local function Msg(text)
    print(("|cff%sAsylum Compagnon|r · %s"):format(NS.ACCENT, text))
end
NS.Msg = Msg

-- ---------- SavedVariables ----------
local function InitDB()
    AsylumCompagnonDB = AsylumCompagnonDB or {}
    for k, v in pairs(DEFAULTS) do
        if AsylumCompagnonDB[k] == nil then AsylumCompagnonDB[k] = v end
    end
    if type(AsylumCompagnonDB.peers) ~= "table" then AsylumCompagnonDB.peers = {} end
    -- purge des pairs fantômes : sans nouvelles depuis 30 jours → oubliés.
    -- Sinon la base gonfle à vie (chaque guildeux croisé y reste avec son
    -- profil SimC complet, jusqu'à ~24 Ko chacun).
    local cutoff = time() - 30 * 24 * 3600
    for key, p in pairs(AsylumCompagnonDB.peers) do
        if math.max(p.lastSeen or 0, p.ts or 0) < cutoff then
            AsylumCompagnonDB.peers[key] = nil
        end
    end
    if type(AsylumCompagnonDB.cdShow) ~= "table" then AsylumCompagnonDB.cdShow = {} end
    NS.db = AsylumCompagnonDB

    -- données par personnage (mémoire des objets déjà possédés)
    AsylumCompagnonCharDB = AsylumCompagnonCharDB or {}
    if type(AsylumCompagnonCharDB.itemsSeen) ~= "table" then AsylumCompagnonCharDB.itemsSeen = {} end
    NS.chardb = AsylumCompagnonCharDB
end

-- ---------- Bouton minimap ----------
local button

local function UpdateButtonPosition()
    if not button then return end
    local angle  = math.rad(NS.db.minimapPos or DEFAULTS.minimapPos)
    local radius = (Minimap:GetWidth() / 2) + 5
    button:ClearAllPoints()
    button:SetPoint("CENTER", Minimap, "CENTER",
        math.cos(angle) * radius, math.sin(angle) * radius)
end
NS.UpdateMinimapButton = UpdateButtonPosition

local function OnDragUpdate()
    local mx, my = Minimap:GetCenter()
    local cx, cy = GetCursorPosition()
    local scale  = Minimap:GetEffectiveScale()
    cx, cy = cx / scale, cy / scale
    NS.db.minimapPos = math.deg(atan2(cy - my, cx - mx)) % 360
    UpdateButtonPosition()
end

-- Rafraîchissement MANUEL (clic gauche du bouton minimap) : régénère mon profil,
-- relit mes stocks, et rediffuse profil + clé + coffre au groupe/guilde. Limité à
-- 1 fois toutes les 10 s pour ne pas spammer le réseau.
local lastManualRefresh = 0
function NS.ManualRefresh()
    local now = (GetServerTime and GetServerTime()) or time()
    local since = now - lastManualRefresh
    if since < 10 then
        if NS.Msg then NS.Msg((NS.L("Rafraîchissement dispo dans %d s.")):format(10 - since)) end
        return
    end
    lastManualRefresh = now
    if NS.UpdateMyProfile then NS.UpdateMyProfile(true) end                    -- profil + hello
    if NS.Vault and NS.Vault.BroadcastMine then NS.Vault.BroadcastMine() end   -- clé + coffre
    if NS.Sync and NS.Sync.BroadcastHello then pcall(NS.Sync.BroadcastHello, true) end -- annonce forcée
    if NS.Msg then NS.Msg(NS.L("Données rafraîchies et annoncées au groupe/guilde.")) end
end

local function CreateMinimapButton()
    if button then return end
    button = CreateFrame("Button", "AsylumCompagnonMinimapButton", Minimap)
    button:SetSize(31, 31)
    button:SetFrameStrata("MEDIUM")
    button:SetFrameLevel(8)
    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    button:RegisterForDrag("LeftButton")

    local ring = button:CreateTexture(nil, "BACKGROUND", nil, -2)
    ring:SetTexture("Interface\\Buttons\\WHITE8x8")
    ring:SetAllPoints()
    ring:SetVertexColor(1, 1, 1, 0.15)
    local ringMask = button:CreateMaskTexture()
    ringMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    ringMask:SetAllPoints(ring)
    ring:AddMaskTexture(ringMask)

    local disc = button:CreateTexture(nil, "BACKGROUND", nil, -1)
    disc:SetTexture("Interface\\Buttons\\WHITE8x8")
    disc:SetPoint("CENTER")
    disc:SetSize(28, 28)
    disc:SetVertexColor(0.055, 0.06, 0.078, 1)
    local discMask = button:CreateMaskTexture()
    discMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    discMask:SetAllPoints(disc)
    disc:AddMaskTexture(discMask)

    local icon = button:CreateTexture(nil, "ARTWORK")
    icon:SetTexture(NS.ICON_ID)
    icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    icon:SetPoint("CENTER")
    icon:SetSize(24, 24)
    local iconMask = button:CreateMaskTexture()
    iconMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
    iconMask:SetAllPoints(icon)
    icon:AddMaskTexture(iconMask)

    button:SetScript("OnEnter", function(self)
        ring:SetVertexColor(0.655, 0.545, 0.98, 1)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:SetText(NS.L("Asylum Compagnon"), 1, 1, 1)
        GameTooltip:AddLine(NS.L("Clic gauche : rafraîchir + annoncer"), 0.72, 0.72, 0.78)
        GameTooltip:AddLine(NS.L("Clic droit : ouvrir / fermer le menu"), 0.72, 0.72, 0.78)
        GameTooltip:AddLine(NS.L("Glisser : déplacer le bouton"), 0.72, 0.72, 0.78)
        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", function()
        ring:SetVertexColor(1, 1, 1, 0.15)
        GameTooltip:Hide()
    end)

    button:SetScript("OnMouseDown", function() icon:SetSize(22, 22) end)
    button:SetScript("OnMouseUp", function() icon:SetSize(24, 24) end)
    button:SetScript("OnClick", function(_, mouseButton)
        if mouseButton == "RightButton" then
            NS.Toggle()          -- clic droit : ouvrir / fermer le menu
        else
            NS.ManualRefresh()   -- clic gauche : rafraîchir + annoncer (max 1×/10 s)
        end
    end)
    button:SetScript("OnDragStart", function(self)
        self:SetScript("OnUpdate", OnDragUpdate)
    end)
    button:SetScript("OnDragStop", function(self)
        self:SetScript("OnUpdate", nil)
    end)

    UpdateButtonPosition()
end

-- ---------- Mises à jour du profil (anti-spam) ----------
-- rapide (3 s) : équipement / talents / spé
local profileTimer
local function QueueProfileUpdate()
    if profileTimer then profileTimer:Cancel() end
    profileTimer = C_Timer.NewTimer(3, function()
        profileTimer = nil
        if InCombatLockdown and InCombatLockdown() then
            QueueProfileUpdate()
            return
        end
        if NS.UpdateMyProfile then NS.UpdateMyProfile(true) end
    end)
end

-- lente (12 s) : or / contenu des sacs
local invTimer
local function QueueInventoryUpdate()
    if invTimer then return end
    invTimer = C_Timer.NewTimer(12, function()
        invTimer = nil
        if InCombatLockdown and InCombatLockdown() then return end
        if NS.UpdateMyProfile then NS.UpdateMyProfile(true) end
    end)
end

local groupTimer
local function QueueGroupHello()
    if groupTimer then return end
    groupTimer = C_Timer.NewTimer(8, function()
        groupTimer = nil
        if NS.Sync and NS.Sync.available then NS.Sync.BroadcastHello(true) end
    end)
end

-- ---------- Événements ----------
local entered = false
local events = CreateFrame("Frame")
events:RegisterEvent("ADDON_LOADED")
events:RegisterEvent("PLAYER_LOGIN")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
events:RegisterEvent("TRAIT_CONFIG_UPDATED")
events:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
events:RegisterEvent("GROUP_ROSTER_UPDATE")
events:RegisterEvent("PLAYER_MONEY")
events:RegisterEvent("BAG_UPDATE_DELAYED")
events:SetScript("OnEvent", function(_, event, arg1)
    if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
        InitDB()
        NS.VERSION = (C_AddOns and C_AddOns.GetAddOnMetadata
            and C_AddOns.GetAddOnMetadata(ADDON_NAME, "Version")) or "0.3.0"
        events:UnregisterEvent("ADDON_LOADED")

    elseif event == "PLAYER_LOGIN" then
        CreateMinimapButton()
        Msg((NS.L("chargé — |cff%s/asylum|r pour ouvrir, ou clique le bouton autour de la minimap.")):format(NS.ACCENT))

    elseif event == "PLAYER_ENTERING_WORLD" then
        if not entered then
            entered = true
            -- à la connexion on ignore qui est là : tous hors ligne, leurs
            -- hellos (dont les réponses au broadcast reply=1) re-verdiront
            -- les vrais en ligne dans les secondes qui suivent
            if NS.Sync and NS.Sync.MarkAllOffline then NS.Sync.MarkAllOffline() end
            -- petit délai : le temps que talents/équipement soient disponibles
            C_Timer.After(4, function()
                if NS.UpdateMyProfile then NS.UpdateMyProfile(false) end
                if NS.Sync and NS.Sync.available then NS.Sync.BroadcastHello(true) end
            end)
            -- heartbeat : rediffusion périodique + balayage des hors ligne
            if NS.Sync and NS.Sync.StartHeartbeat then NS.Sync.StartHeartbeat() end
        end

    elseif event == "PLAYER_EQUIPMENT_CHANGED"
        or event == "TRAIT_CONFIG_UPDATED" then
        if entered then QueueProfileUpdate() end

    elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
        if entered and (not arg1 or arg1 == "player") then QueueProfileUpdate() end

    elseif event == "GROUP_ROSTER_UPDATE" then
        if entered then QueueGroupHello() end

    elseif event == "PLAYER_MONEY" or event == "BAG_UPDATE_DELAYED" then
        if entered then QueueInventoryUpdate() end
    end
end)

-- ---------- Commandes slash ----------
SLASH_ASYLUMCOMPAGNON1 = "/asylum"
SlashCmdList.ASYLUMCOMPAGNON = function(msg)
    msg = msg or ""
    local cmd = (msg:match("^(%S*)") or ""):lower()

    if cmd == "" then
        NS.Toggle()
    elseif cmd == "auras" then
        -- liste les spellID de tes buffs (pour compléter les tables)
        Msg(NS.L("tes buffs actuels :"))
        if C_UnitAuras and C_UnitAuras.GetBuffDataByIndex then
            for i = 1, 60 do
                local ok, aura = pcall(C_UnitAuras.GetBuffDataByIndex, "player", i)
                if not ok or not aura then break end
                local sid = aura.spellId
                if sid and not (issecretvalue and issecretvalue(sid)) then
                    print(("   %d"):format(sid))   -- pas de aura.name : « secret » en Midnight
                end
            end
        end
    elseif cmd == "cd" then
        -- liste les CD suivis de ta classe (aide au réglage de NS.COOLDOWNS)
        local _, myClass = UnitClass("player")
        Msg((NS.L("cooldowns suivis pour %s :")):format(NS.ClassLabel and NS.ClassLabel(myClass) or myClass or "?"))
        for sid, info in pairs(NS.COOLDOWNS or {}) do
            if info.class == myClass then
                print(("   %d — %s (%ds, %s)%s"):format(sid, NS.SpellName(sid, info.label), info.cd, info.type,
                    NS.IsCDShown and NS.IsCDShown(sid) and NS.L(" |cff22c55e[affiché]|r") or ""))
            end
        end
    elseif cmd == "keys" or cmd == "key" or cmd == "cles" or cmd == "clés" then
        -- clés M+ du groupe (toi + membres avec l'addon) dans le chat
        if NS.Vault and NS.Vault.PrintGroupKeys then NS.Vault.PrintGroupKeys() end
    elseif cmd == "keydebug" then
        -- diagnostic de la détection de clé (APIs + liens des sacs)
        if NS.Vault and NS.Vault.DebugKey then NS.Vault.DebugKey() end
    elseif cmd == "ping" then
        -- test de communication : envoie un ping au groupe/guilde, chaque
        -- client répond un pong. Isole qui peut émettre/recevoir.
        if NS.Sync and NS.Sync.SendPing then NS.Sync.SendPing() end
    elseif cmd == "sync" then
        if NS.Sync and NS.Sync.available then
            if NS.UpdateMyProfile then NS.UpdateMyProfile(false) end
            NS.Sync.BroadcastHello(true)
            Msg(NS.L("annonce envoyée au groupe et à la guilde."))
        else
            Msg(NS.L("synchro indisponible (canal de communication non enregistré)."))
        end
    elseif cmd == "reset" then
        for k, v in pairs(DEFAULTS) do NS.db[k] = v end
        NS.db.peers = {}
        NS.db.kickPos = nil
        NS.db.kickScale = nil
        NS.db.uiScale = nil
        NS.db.uiPos = nil
        NS.db.blAlertPos = nil
        if NS.Stats and NS.Stats.ResetBLPosition then NS.Stats.ResetBLPosition() end
        if _G["AsylumCompagnonFrame"] then _G["AsylumCompagnonFrame"]:SetScale(1) end
        UpdateButtonPosition()
        if NS.Kicks then
            NS.Kicks.ResetPosition()
            NS.Kicks.SetEnabled(true)
            if NS.Kicks.ApplyStyle then NS.Kicks.ApplyStyle() end
        end
        if NS.Cooldowns then
            NS.Cooldowns.ResetPosition()
            NS.Cooldowns.SetEnabled(true)
            if NS.Cooldowns.ApplyStyle then NS.Cooldowns.ApplyStyle() end
        end
        Msg(NS.L("réglages remis par défaut (liste d'amis vidée)."))
    else
        Msg(NS.L("commandes : |cffffffff/asylum|r ouvrir · |cffffffff/asylum keys|r clés du groupe · |cffffffff/asylum ping|r test de comms · |cffffffff/asylum sync|r annoncer · |cffffffff/asylum reset|r"))
    end
end

-- ---------- Compartiment d'addons (tiroir de la minimap) ----------
function AsylumCompagnon_OnCompartmentClick()
    NS.Toggle()
end
