-- Sleek RP - Warlock Pets (Events.lua)
-- Déclenche les dialogues du Core (SRPWL.TrySpeak) en FR/EN.
-- Points clés:
--  - Délai 1.5s sur certaines emotes + MapOpened + QuestCompleted
--  - Pêche fiable (canal + start + vérif UnitChannelInfo) et priorité du catch dans LOOT_OPENED
--  - RaidWipe: file d'attente si le familier a disparu; flush à la réinvo
--  - ZoneDiscovered: plus d'annonce au login/reload (on marque seulement)
--  - FIX: activeBuffs déclaré AVANT son usage (évite l’erreur nil)
--  - NEW: dialogue_rate (multiplie / divise les délais et pings de zone)
--  - NEW: SRPWL.TestRandomUtterance() pour l’UI d’options
--  - NEW: SpellCastMajor (Classic-safe) + anti-spam dédié 40s

local ADDON_NAME, SRPWL = ...
if not SRPWL then return end

local f = CreateFrame("Frame")
local DB = _G.SleekRP_WLP_DB or (_G.SleekRP_WLP_DB or {})
_G.SleekRP_WLP_DB = DB
DB.seen_regions = DB.seen_regions or {}

-- === NEW defaults liés aux options ===
DB.dialogue_rate = DB.dialogue_rate or 1.0  -- 0.25..2.0 recommandé

-- === FIX: table des buffs actifs (déclarée tôt, visible par les fonctions) ===
local activeBuffs = {}

-- ===== helpers =====
local function IsDebug() return DB and DB.debug == true end
local function log(...) if IsDebug() then print("|cff88fSleekRP:|r", ...) end end
local function UnitAlive(u) return UnitExists(u) and not UnitIsDeadOrGhost(u) end

-- --- Rate helpers ------------------------------------------------------------
-- Utilise SRPWL.GetDialogueRate() si dispo, sinon DB.dialogue_rate
local function GetRate()
  local r = (SRPWL and SRPWL.GetDialogueRate and SRPWL.GetDialogueRate()) or DB.dialogue_rate or 1.0
  r = tonumber(r) or 1.0
  if r < 0.1 then r = 0.1 end
  if r > 3.0 then r = 3.0 end
  return r
end

-- Réduit un cooldown quand le rate augmente (parle plus souvent)
local function EffectiveCooldown(base)
  local rate = GetRate()
  local cd = (tonumber(base) or 1.0) / rate
  -- plancher anti-spam pour les timers longs
  if cd < 0.5 then cd = 0.5 end
  return cd
end
-- ---------------------------------------------------------------------------

local function SpeakNow(key)
  if not key then return end
  if UnitExists("pet") then
    if SRPWL and SRPWL.TrySpeak then SRPWL.TrySpeak(key) end
  else
    -- file l'event pour plus tard (ex: mort → pet disparu)
    f._pending = f._pending or {}
    f._pending[key] = GetTime()
    if IsDebug() then log("Queue event (no pet):", key) end
  end
end

-- flush des events en attente à la prochaine présence du pet
local function FlushPending()
  if not f._pending then return end
  local keys = {}
  for k,_ in pairs(f._pending) do table.insert(keys, k) end
  table.sort(keys) -- ordre stable
  f._pending = nil
  for _,k in ipairs(keys) do
    C_Timer.After(0.05, function() SpeakNow(k) end)
  end
end

-- ===== Délai 1.5s pour certains events =====
local DELAYED_EVENTS = {
  Emote_Roar = true, Emote_Laugh = true, Emote_Yes = true, Emote_No = true,
  Emote_Cheer = true, Emote_Cry = true, Emote_Whistle = true,
  MapOpened = true, QuestCompleted = true,
  -- Proposés (décommente pour activer) :
  -- Emote_Salute = true, Emote_Clap = true, Emote_WaveKiss = true,
}

local function Emit(key)
  if DELAYED_EVENTS[key] then
    C_Timer.After(1.5, function() SpeakNow(key) end)
  else
    SpeakNow(key)
  end
end

local function TrySpeak(key)
  if not key then return end
  Emit(key)
end

-- ===== String utils =====
local function stripAccents(s)
  if not s then return "" end
  s = s:gsub("[àáâãäå]", "a"):gsub("ç", "c")
       :gsub("[èéêë]", "e"):gsub("[ìíîï]", "i")
       :gsub("[òóôöõ]", "o"):gsub("[ùúûü]", "u")
       :gsub("ÿ", "y"):gsub("œ", "oe"):gsub("æ", "ae")
       :gsub("À","A"):gsub("Â","A"):gsub("Ä","A"):gsub("É","E"):gsub("È","E"):gsub("Ê","E"):gsub("Ë","E")
       :gsub("Î","I"):gsub("Ï","I"):gsub("Ô","O"):gsub("Ö","O"):gsub("Ù","U"):gsub("Û","U"):gsub("Ü","U")
       :gsub("Œ","OE"):gsub("Æ","AE"):gsub("’","'")
  return s
end
local function norm(s)
  if not s or s == "" then return "" end
  s = stripAccents(s)
  s = s:gsub("[^%w%s%-']", "")
  return string.lower(s)
end
local function compactKeyBase(s)
  s = stripAccents(s or "")
  s = s:gsub("[^%w]", "")
  return s
end
local function trim(s) return (s and s:gsub("^%s+",""):gsub("%s+$","") or "") end

-- ===== Safe spell info/name =====
local function SpellInfo(spellID)
  if C_Spell and C_Spell.GetSpellInfo then
    return C_Spell.GetSpellInfo(spellID)
  end
  if GetSpellInfo then
    local name, _, _, castTime = GetSpellInfo(spellID)
    if name then return { name = name, castTime = castTime } end
  end
  return nil
end
local function SpellName(spellID)
  if C_Spell and C_Spell.GetSpellName then
    local n = C_Spell.GetSpellName(spellID)
    if n and n ~= "" then return n end
  end
  if GetSpellInfo then
    local n = GetSpellInfo(spellID)
    if n and n ~= "" then return n end
  end
  return nil
end

-- ===== Regions (capitals/hubs + parent) =====
local ZONE_ALIAS = {
  ["hurlevent"]="Zone_Stormwind", ["forgefer"]="Zone_Ironforge",
  ["lespitonsdutonnerre"]="Zone_ThunderBluff", ["pitonsdutonnerre"]="Zone_ThunderBluff",
  ["exodar"]="Zone_Exodar", ["lexodar"]="Zone_Exodar",
  ["lunedargent"]="Zone_Silvermoon", ["orgrimmar"]="Zone_Orgrimmar",
  ["darnassus"]="Zone_Darnassus", ["shattrath"]="Zone_Shattrath",
  ["dalaran"]="Zone_Dalaran", ["gilneas"]="Zone_Gilneas", ["kezan"]="Zone_Kezan",
  ["oribos"]="Zone_Oribos", ["valdrakken"]="Zone_Valdrakken",
  ["boralus"]="Zone_Boralus", ["dazarlor"]="Zone_Dazaralor", ["dazaralor"]="Zone_Dazaralor",
  ["marchedelouest"]="Zone_Westfall", ["dunmorogh"]="Zone_DunMorogh",
  ["lochmodan"]="Zone_LochModan", ["gorgedesventbrulants"]="Zone_SearingGorge",
  ["foretdelwynn"]="Zone_ElwynnForest", ["foretdelwyn"]="Zone_ElwynnForest",
  ["tanaris"]="Zone_Tanaris", ["gangrebois"]="Zone_Felwood", ["feralas"]="Zone_Feralas",
  ["steppesardentes"]="Zone_BurningSteppes", ["azshara"]="Zone_Azshara",
  ["maleterres"]="Zone_Plaguelands", ["serresrocheuses"]="Zone_StonetalonMountains",
  ["lesserresrocheuses"]="Zone_StonetalonMountains",
  ["valleedestrangleronce"]="Zone_StranglethornVale",
  ["ungoro"]="Zone_UnGoroCrater", ["crateredungoro"]="Zone_UnGoroCrater",
  ["berceaudelhiver"]="Zone_Winterspring", ["desolace"]="Zone_Desolace",
  ["silithus"]="Zone_Silithus",
  ["peninsuledesflammesinfernales"]="Zone_HellfirePeninsula",
  ["marecagedezangar"]="Zone_Zangarmarsh", ["razdeneant"]="Zone_Netherstorm",
  ["valleedombrlune"]="Zone_ShadowmoonValley", ["valleedombrelune"]="Zone_ShadowmoonValley",
  ["lestranchantes"]="Zone_BladesEdgeMountains", ["nagrand"]="Zone_Nagrand",
  -- EN fallback
  ["stormwind"]="Zone_Stormwind", ["ironforge"]="Zone_Ironforge",
  ["thunderbluff"]="Zone_ThunderBluff", ["silvermoon"]="Zone_Silvermoon",
  ["orgrimmar_en"]="Zone_Orgrimmar",
}
local function tryKeyFromName(name)
  if not name or name == "" then return nil end
  local alias = ZONE_ALIAS[norm(name):gsub("[%s%-']", "")]
  if alias then return alias end
  local compact = compactKeyBase(name)
  if compact ~= "" and SRPWL.HasEventKey then
    local candidate = "Zone_" .. compact
    if SRPWL.HasEventKey(candidate) then return candidate end
  end
  return nil
end
local function getRegionKey()
  local id = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
  local info = id and C_Map.GetMapInfo and C_Map.GetMapInfo(id)
  local guard = 0
  if info and info.name then
    local k = tryKeyFromName(info.name); if k then return k end
  end
  while info and info.parentMapID and guard < 10 do
    guard = guard + 1
    info = C_Map.GetMapInfo(info.parentMapID)
    if info and info.name then
      local k2 = tryKeyFromName(info.name); if k2 then return k2 end
    end
  end
  local regionName = (GetRealZoneText and GetRealZoneText()) or (GetZoneText and GetZoneText()) or ""
  local k3 = tryKeyFromName(regionName)
  if k3 then return k3 end
  return (regionName ~= "") and ("Zone_" .. compactKeyBase(regionName)) or nil
end

-- ===== throttles & states =====
local lastMoney = 0
local lastKeyAt = {}
-- Note: si tu veux que certains throttles soient influencés par dialogue_rate,
-- passe scaleWithRate=true au 3e param.
local function throttle(key, window, scaleWithRate)
  local t = GetTime()
  window = tonumber(window) or 1.5
  if scaleWithRate then
    window = EffectiveCooldown(window)
  end
  local last = lastKeyAt[key] or 0
  if (t - last) < window then return true end
  lastKeyAt[key] = t
  return false
end

local vendorOpen   = false
local wasInGroup   = IsInGroup() or IsInRaid()
local wasMounted   = (IsMounted and IsMounted()) or false
local lastKillAt   = 0
local mapHooked    = false

-- 🎣 pêche
local fishingActive = false
local lastFishingAt = 0

-- IDs vus sur plusieurs versions (retail/classic).
local FISHING_IDS = {
  [131474]=true, [131476]=true, -- Retail (variantes)
  [7620]=true, [7731]=true, [7732]=true, [18248]=true, [33095]=true, -- Classic / anciens rangs
}
-- tokens FR/EN pour nom du sort (normalisé)
local FISH_TOKENS = { "fish", "pech", "peche" } -- norm() enlève l’accent → “peche”
local function isFishingName(n)
  n = norm(n or "")
  for _,k in ipairs(FISHING_TOKENS or FISH_TOKENS) do if n:find(k, 1, true) then return true end end
  return false
end
-- Vérif différée via UnitChannelInfo (au cas où START arrive avant que le nom soit résolu)
local function verifyFishingChannelSoon()
  C_Timer.After(0.05, function()
    local name = UnitChannelInfo and UnitChannelInfo("player")
    if name and isFishingName(name) then
      fishingActive = true
      if not throttle("FishingStarted_uc", 1.0) then TrySpeak("FishingStarted") end
    end
  end)
end

-- ===== Region ticker (3–4 min, jitter) =====
f._regionTimer = nil
f._currentRegionKey = nil
local function cancelRegionTimer() if f._regionTimer and f._regionTimer.Cancel then f._regionTimer:Cancel() end f._regionTimer = nil end
local function scheduleRegionPing(regionKey)
  cancelRegionTimer()
  if not regionKey then return end
  local function nextDelay()
    -- base 180..240s → modulé par dialogue_rate
    local base = 180 + math.random(0, 60)
    return EffectiveCooldown(base)
  end
  local function ping()
    if getRegionKey() == regionKey then
      log("Region ping ->", regionKey, "(rate=", GetRate(), ")")
      TrySpeak(regionKey)
      f._regionTimer = C_Timer.NewTimer(nextDelay(), ping)
    end
  end
  f._regionTimer = C_Timer.NewTimer(nextDelay(), ping)
end

-- ===== Raid buffs & eat/drink =====
SRPWL.BUFF = SRPWL.BUFF or {}
local SRPWL_BUFF = SRPWL.BUFF
SRPWL_BUFF.durationThreshold = SRPWL_BUFF.durationThreshold or 300 -- 5 min

local RAID_CANONICAL_IDS = {
  1459,23028, 21562,21564,14752,27681,976,27683,
  1126,21849, 6673, 20217,19742,19740,20911, 381748
}
local RAID_NAME_FALLBACKS = {
  "Arcane Intellect","Arcane Brilliance",
  "Power Word: Fortitude","Prayer of Fortitude",
  "Mark of the Wild","Gift of the Wild",
  "Battle Shout",
  "Blessing of Kings","Blessing of Wisdom","Blessing of Might","Blessing of Sanctuary",
  "Divine Spirit","Prayer of Spirit",
  "Shadow Protection","Prayer of Shadow Protection",
  "Blessing of the Bronze",
  "Intelligence des arcanes","Brillance des arcanes",
  "Mot de pouvoir : Robustesse","Prière de Robustesse",
  "Marque du fauve","Don du fauve",
  "Cri de guerre",
  "Bénédiction des rois","Bénédiction de sagesse","Bénédiction de puissance","Bénédiction de sanctuaire",
  "Esprit divin","Prière d'esprit",
  "Protection contre l’Ombre","Prière de Protection contre l’Ombre","Protection contre l'Ombre",
  "Bénédiction du bronze",
}
local RAID_NAMESET = nil
local function buildRaidNameSet()
  if RAID_NAMESET then return RAID_NAMESET end
  RAID_NAMESET = {}
  for _, id in ipairs(RAID_CANONICAL_IDS) do
    local info = SpellInfo(id); local name = info and info.name
    if name and name ~= "" then RAID_NAMESET[norm(name)] = true end
  end
  for _, n in ipairs(RAID_NAME_FALLBACKS) do RAID_NAMESET[norm(n)] = true end
  return RAID_NAMESET
end
local function isRaidBuffLongEnough(auraName, aDur)
  local names = buildRaidNameSet()
  local n = norm(auraName or "")
  local th = tonumber(SRPWL_BUFF.durationThreshold) or 300
  if not names[n] then return false end
  if not aDur or aDur == 0 then return true end
  return aDur >= th
end

local EAT_STEMS = {
  "eat","food","well fed","feast","banquet","picnic","refreshment",
  "nourri","nourrit","nourriture","repas","restaure","régén","regen","régénère","regener",
  "biscuit","gateau","gâteau","riz","pain","rago","ragou","ragoût","ragout",
  "bois","boisson","drink","thé","the ","grog","lait","eau",
}
local function isEatingOrDrinkingBuff(name)
  local n = norm(name or "")
  for _, k in ipairs(EAT_STEMS) do if n:find(k, 1, true) then return true end end
  return false
end

local function forEachBuff(unit, cb)
  if AuraUtil and AuraUtil.ForEachAura then
    AuraUtil.ForEachAura(unit, "HELPFUL", nil, cb)
  else
    local i = 1
    while true do
      local name, _, _, _, duration, expirationTime, _, _, _, spellId = UnitBuff(unit, i)
      if not name then break end
      cb({name=name, spellId=spellId, duration=duration, expirationTime=expirationTime})
      i = i + 1
    end
  end
end

local function scanBuffsAndDetect(unit)
  if unit ~= "player" then return end
  local foundEatDrink = false
  local seen = {}
  forEachBuff("player", function(a)
    local id = a.spellId or 0
    seen[id] = true
    if isEatingOrDrinkingBuff(a.name) then foundEatDrink = true end
    if not activeBuffs[id] then
      if isRaidBuffLongEnough(a.name, a.duration) then
        activeBuffs[id] = true
        if not throttle("BuffReceived", 0.5) then TrySpeak("BuffReceived") end
      end
    end
  end)
  for id in pairs(activeBuffs) do if not seen[id] then activeBuffs[id] = nil end end
  if foundEatDrink and not throttle("EatingDrinking", 3.0) then TrySpeak("EatingDrinking") end
end

-- ===== Keywords (spells & emotes) =====
local KW = {
  hearthstone = { "hearth", "pierre de foyer", "foyer", "heimkehr", "hogar" },
  healthstone = { "healthstone", "pierre de soins", "piedra de salud", "gesundheitsstein" },
  soulstone   = { "soulstone", "pierre d'ame", "pierre d'âme", "soulwell", "puits d'ame", "puits d'âme", "piedra de alma", "seelenstein" },
  summon_rit  = { "ritual of summoning", "rituel d'invocation", "ritual de invocación", "ritual der beschwörung" },
  gateway     = { "demonic gateway", "portail demon", "portail démon", "dämonisches portal", "portal demoni", "gateway" },
  potion      = { "potion ", " potion", "poción", "trank" },
  sum_demon   = { "invocation", "invoque", "summon", "beschwör" },

  -- emotes
  emote_bow      = { "s'incline", " vous inclinez", " incline", "bow" },
  emote_kneel    = { "agenou", "met un genou", "mettez a genoux", " a genoux", "genoux", "kneel" },
  emote_laugh    = { " vous riez", " riez", " rigole", "rire", "rit ", "rican", "moqu", "laugh", "lol", "ptdr", "mdr" },
  emote_thank    = { "remerc", "merci", "thank" },
  emote_wink     = { "clin d", "cligne de l", "wink" },
  emote_roar     = { "rug", "roar", "hurl" },
  emote_cry      = { "pleur", "sanglot", "larmes", "cry" },
  emote_beg      = { "supplie", "supplier", "implor", "pitoy", "pitie", "beg" },
  emote_pat      = { "tapote","tapoter","tapotes","tape dans le dos","donne une tape dans le dos"," tapote le dos "," tapote dans le dos "," pat " },
  emote_hug      = { "câlin","calin","serre","enlace","étreint","etreint","prend dans ses bras","hug","umarm" },
  emote_rude     = { "grossier","obscen","obscène","rude","doigt" },
  emote_sit      = { "s'assoit"," vous asseyez"," assis","sit " },
  emote_sleep    = { "vous vous endorm","s'endorm","endorm","dort","sommeil","sleep" },
  emote_yes      = { "hoche","acquiesc"," opine"," oui"," nod","signe de tete","affirmatif" },
  emote_no       = { "secoue"," non"," shake" },
  emote_cheer    = { "acclame","acclamez","hurle de joie","hurrah","hourra","hourras","vive","vivats","bravo","encourag","cheer","jubelt" },
  emote_whistle  = { "siffle","whistle","appel" },
  emote_point    = { "pointe","montre du doigt"," point ","zeigt" },
  emote_shrug    = { "hausse les epaules","hausse les épaules","haussant les epaules","haussant les épaules","haussez les","shrug" },
  emote_clap     = { "applaud","clap","klatscht" },
  emote_salute   = { "salut milita","salute" },
  emote_kiss     = { "bisou","baiser","embrasse","kiss","envoie un baiser","souffle un baiser" },
  emote_pet      = { "caresse","acaricia","streichelt"," pet " },
  emote_dance    = { "danse","dance","tanzt","baila" },
  emote_hi       = { "bonjour","salut","salue","coucou","wave","winkt","fait signe","faites signe","fait un signe","fais signe" },
  emote_love     = { "aime","amour","love" },
}
local function matchAny(s, list)
  s = norm(s or "")
  for _, k in ipairs(list) do if s:find(k, 1, true) then return true end end
  return false
end

local EMOTE_ORDER = {
  { key="Emote_Bow",      kw=KW.emote_bow     },
  { key="Emote_Kneel",    kw=KW.emote_kneel   },
  { key="Emote_Laugh",    kw=KW.emote_laugh   },
  { key="Emote_Thank",    kw=KW.emote_thank   },
  { key="Emote_Wink",     kw=KW.emote_wink    },
  { key="Emote_Roar",     kw=KW.emote_roar    },
  { key="Emote_Cry",      kw=KW.emote_cry     },
  { key="Emote_Beg",      kw=KW.emote_beg     },
  { key="Emote_Pat",      kw=KW.emote_pat     },
  { key="Emote_Hug",      kw=KW.emote_hug     },
  { key="Emote_Rude",     kw=KW.emote_rude    },
  { key="Emote_Sit",      kw=KW.emote_sit     },
  { key="Emote_Sleep",    kw=KW.emote_sleep   },
  { key="Emote_Yes",      kw=KW.emote_yes     },
  { key="Emote_No",       kw=KW.emote_no      },
  { key="Emote_Cheer",    kw=KW.emote_cheer   },
  { key="Emote_Whistle",  kw=KW.emote_whistle },
  { key="Emote_Point",    kw=KW.emote_point   },
  { key="Emote_Shrug",    kw=KW.emote_shrug   },
  { key="Emote_Clap",     kw=KW.emote_clap    },
  { key="Emote_Salute",   kw=KW.emote_salute  },
  { key="Emote_WaveKiss", kw=KW.emote_kiss    },
  { key="Emote_Pet",      kw=KW.emote_pet     },
  { key="Emote_Dance",    kw=KW.emote_dance   },
  { key="Emote_Hi",       kw=KW.emote_hi      },
  { key="Emote_Love",     kw=KW.emote_love    },
}

-- ===== Soulstone IDs =====
local SOULSTONE_IDS = { [20707]=true }

-- ===== SpellCastMajor (Classic-safe) =====
local MAJOR_THROTTLE_WINDOW = 40.0 -- secondes, non modulé par dialogue_rate

-- START (longs / rares) → Curse of Doom, Soul Fire, Ritual of Doom, Inferno
local MAJOR_START_IDS = {
  [603]=true,     -- Curse of Doom
  [6353]=true,    -- Soul Fire (Rank 1)
  [17924]=true,   -- Soul Fire (Rank 2)
  [18540]=true,   -- Ritual of Doom
  [1122]=true,    -- Inferno (Summon Infernal)
}
local MAJOR_START_TOKENS = {
  "curse of doom","malediction funeste","malédiction funeste",
  "soul fire","feu de l ame","feu de l'ame",
  "ritual of doom","rituel funeste",
  "inferno","infernal","invocation d un infernal","invocation d'un infernal","invoque un infernal",
}

-- SUCCEEDED (instants / courts) → Enslave Demon, Howl of Terror, Death Coil (WL)
local MAJOR_SUCC_IDS = {
  [1098]=true,    -- Enslave Demon
  [5484]=true,    -- Howl of Terror (Rank 1)
  [17928]=true,   -- Howl of Terror (Rank 2)
  [6789]=true,    -- Death Coil (Rank 1, Warlock)
  [17925]=true,   -- Death Coil (Rank 2)
  [17926]=true,   -- Death Coil (Rank 3)
}
local MAJOR_SUCC_TOKENS = {
  "enslave demon","asservir demon","asservir démon",
  "howl of terror","hurlement de terreur",
  "death coil","voile de mort",
}

local function isInIdSet(spellID, set) return (spellID and set[spellID]) and true or false end
local function hasAnyToken(name, tokens)
  local n = norm(name or "")
  for _,t in ipairs(tokens) do if n:find(norm(t), 1, true) then return true end end
  return false
end
local function isMajorStart(spellID, name)
  return isInIdSet(spellID, MAJOR_START_IDS) or hasAnyToken(name, MAJOR_START_TOKENS)
end
local function isMajorSucc(spellID, name)
  return isInIdSet(spellID, MAJOR_SUCC_IDS) or hasAnyToken(name, MAJOR_SUCC_TOKENS)
end
local function FireSpellCastMajor()
  if not throttle("SpellCastMajor", MAJOR_THROTTLE_WINDOW, false) then
    TrySpeak("SpellCastMajor")
  end
end

-- ===== Group wipe detection =====
local function IsFullGroupDead()
  if IsInRaid() then
    local n = GetNumGroupMembers()
    if n and n > 0 then
      for i=1,n do if UnitAlive("raid"..i) then return false end end
      return true
    end
  elseif IsInGroup() then
    local n = GetNumSubgroupMembers()
    for i=1,n do if UnitAlive("party"..i) then return false end end
    if UnitAlive("player") then return false end
    return n > 0
  end
  return false
end

-- ===== Core handler =====
local function OnEvent(self, event, ...)
  if event == "PLAYER_LOGIN" then
    activeBuffs = {} -- reset propre au login (évite états résiduels)
    lastMoney = GetMoney() or 0
    wasInGroup = IsInGroup() or IsInRaid()
    wasMounted = (IsMounted and IsMounted()) or false
    buildRaidNameSet()
    fishingActive, lastFishingAt = false, 0

    -- hook /sit via DoEmote
    if not f._hookedEmote and type(DoEmote) == "function" then
      f._hookedEmote = true
      hooksecurefunc("DoEmote", function(token)
        if token and tostring(token):upper() == "SIT" then
          if not throttle("Emote_Sit_Hook", 2.0) then TrySpeak("Emote_Sit") end
        end
      end)
    end

    -- Hook carte → MapOpened (avec délai)
    if not mapHooked and WorldMapFrame and WorldMapFrame:IsObjectType("Frame") then
      mapHooked = true
      WorldMapFrame:HookScript("OnShow", function()
        if not throttle("MapOpened", 1.0) then Emit("MapOpened") end
      end)
    end

    -- Région init + ticker + (NE PAS parler ZoneDiscovered au login)
    local rkey = getRegionKey()
    if rkey then
      f._currentRegionKey = rkey
      if not DB.seen_regions[rkey] then DB.seen_regions[rkey] = true end -- on marque mais on ne parle pas
      TrySpeak(rkey) -- retire cette ligne si tu ne veux aucune annonce de région au login
      scheduleRegionPing(rkey)
    end

  elseif event == "PLAYER_MOUNT_DISPLAY_CHANGED" then
    local nowMounted = (IsMounted and IsMounted()) or false
    if nowMounted and not wasMounted then TrySpeak("MountSummoned") end
    wasMounted = nowMounted

  elseif event == "PLAYER_LEVEL_UP" then
    TrySpeak("LevelUp")

  -- ===== Vendor & Gold =====
  elseif event == "MERCHANT_SHOW" then
    vendorOpen = true; TrySpeak("UsingVendor")
    if CanMerchantRepair and CanMerchantRepair() then TrySpeak("RepairingGear") end

  elseif event == "MERCHANT_CLOSED" then
    vendorOpen = false

  elseif event == "PLAYER_MONEY" then
    local m = GetMoney() or 0
    if m > lastMoney then if not throttle("Event_GoldReceived", 0.4) then TrySpeak("Event_GoldReceived") end end
    lastMoney = m

  -- ===== Bank / Mail / AH / Transmog / Trainer / Taxi =====
  elseif event == "BANKFRAME_OPENED" or event == "GUILDBANKFRAME_OPENED" then
    TrySpeak("BankUsed")
  elseif event == "MAIL_SHOW" then
    TrySpeak("MailboxUsed")
  elseif event == "AUCTION_HOUSE_SHOW" then
    TrySpeak("AuctionHouseUsed")
  elseif event == "TRANSMOGRIFY_OPEN" then
    TrySpeak("TransmogUsed")
  elseif event == "TRAINER_SHOW" then
    TrySpeak("TrainingClass")

  elseif event == "TAXIMAP_OPENED" then
    f._taxiOpen = true
  elseif event == "TAXIMAP_CLOSED" then
    f._taxiOpen = false
    C_Timer.After(0.2, function() if UnitOnTaxi and UnitOnTaxi("player") then TrySpeak("FlightPathTaken") end end)

  -- ===== Rested (Inn) =====
  elseif event == "PLAYER_UPDATE_RESTING" then
    if IsResting() and not throttle("InnResting", 2.0) then TrySpeak("InnResting") end

  -- ===== Boss end / wipe =====
  elseif event == "ENCOUNTER_END" then
    local _, _, _, _, endStatus = ...
    if endStatus == 1 then
      TrySpeak("BossKill")
    else
      Emit("RaidWipe") -- file si pas de pet; jouera au retour du familier
    end

  -- ===== Régions (pas de spam sous-zones) + ZoneDiscovered 1ère fois (post-login uniquement) =====
  elseif event == "ZONE_CHANGED" or event == "ZONE_CHANGED_NEW_AREA" or event == "ZONE_CHANGED_INDOORS" then
    local rkey = getRegionKey()
    if rkey and rkey ~= f._currentRegionKey then
      if not DB.seen_regions[rkey] then DB.seen_regions[rkey] = true; TrySpeak("ZoneDiscovered") end
      TrySpeak(rkey)
      f._currentRegionKey = rkey
      scheduleRegionPing(rkey)
    end

  -- ===== Emotes (FR/EN, joueur uniquement) =====
  elseif event == "CHAT_MSG_TEXT_EMOTE" or event == "CHAT_MSG_EMOTE" then
    local msg, sender = ...
    local function playerMatchesSender(s)
      local myName, myRealm = UnitName("player"), GetRealmName()
      if not s or s=="" then return false end
      return (s==myName) or (myRealm and s==(myName.."-"..myRealm))
    end
    local function isSelfEmote(m,s)
      if playerMatchesSender(s) then return true end
      local l = norm(trim(m or "")); return l:find("^you%s") or l:find("^vous%s") or l:find("^vous%p")
    end
    if not isSelfEmote(msg, sender) then return end
    local l = norm(msg or "")
    for _, e in ipairs(EMOTE_ORDER) do
      if matchAny(l, e.kw) then Emit(e.key); return end
    end
    TrySpeak("Emote_Any")

  -- ===== Combat log: kills + Soulstone =====
  elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then
    local _, subEvent, _, srcGUID, _, _, _, destGUID, _, _, _, spellId, spellName = CombatLogGetCurrentEventInfo()
    -- Kill → distingue chest/corpse
    if (subEvent == "PARTY_KILL" or subEvent == "UNIT_DIED") and srcGUID == UnitGUID("player") then
      lastKillAt = GetTime()
    end
    -- Soulstone (cast success or aura applied by player)
    if (subEvent == "SPELL_CAST_SUCCESS" or subEvent == "SPELL_AURA_APPLIED") and srcGUID == UnitGUID("player") then
      local n = norm(spellName or "")
      if SOULSTONE_IDS[spellId] or matchAny(n, KW.soulstone) then
        if not throttle("SoulstoneApplied", 2.0) then log("Soulstone via CLEU ->", spellId, spellName or "?"); TrySpeak("SoulstoneApplied") end
      end
    end

  elseif event == "LOOT_OPENED" then
    -- 🎣 priorité pêche
    if fishingActive or (GetTime() - (lastFishingAt or 0)) < 8 then
      if not throttle("FishingCatch", 1.0) then TrySpeak("FishingCatch") end
      fishingActive = false
      return
    end
    -- Sinon logique coffre/cadavre
    local sinceKill = GetTime() - (lastKillAt or 0)
    if sinceKill < 10 then TrySpeak("OpeningCorpseLoot") else TrySpeak("GatheringLootChest") end

  -- ===== Casts (canal & normal) =====
  elseif event == "UNIT_SPELLCAST_CHANNEL_START" then
    local unit, _, spellID = ...
    if unit == "player" then
      local info = SpellInfo(spellID)
      local name = (info and info.name) or SpellName(spellID) or ""
      local n = norm(name)
      -- 🎣 Pêche via canal
      if FISHING_IDS[spellID] or isFishingName(n) then
        fishingActive = true
        if not throttle("FishingStarted", 1.0) then TrySpeak("FishingStarted") end
        verifyFishingChannelSoon()
        return
      end
      -- Manger/boire via canal
      if matchAny(n, {"mang","nourrit","food","feast","banqu","repas"}) or matchAny(n, {"boi","drink","the ","thé","grog","lait","eau"}) then
        if not throttle("EatingDrinking_cast", 2.0) then TrySpeak("EatingDrinking") end
      end
    end

  elseif event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
    if ... == "player" then verifyFishingChannelSoon() end

  elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
    local unit = ...
    if unit == "player" then
      fishingActive = false
      lastFishingAt = GetTime()
    end

  elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_SUCCEEDED" then
    local unit, _, spellID = ...
    if unit ~= "player" then return end
    local info = SpellInfo(spellID)
    local spellName = info and info.name or SpellName(spellID) or ""
    local n = norm(spellName)

    -- 🎣 fallback: marquer l’instant / vérifier canal peu après
    if FISHING_IDS[spellID] or isFishingName(n) then
      if event == "UNIT_SPELLCAST_SUCCEEDED" then
        lastFishingAt = GetTime()
      else
        verifyFishingChannelSoon()
        if not throttle("FishingStarted_start", 1.0) then TrySpeak("FishingStarted") end
      end
      return
    end

    -- Consos instantanées food/drink via SUCCEEDED
    if event == "UNIT_SPELLCAST_SUCCEEDED" then
      if matchAny(n, EAT_STEMS) then
        if not throttle("EatingDrinking_item", 2.0) then TrySpeak("EatingDrinking") end
      end
    end

    -- Métiers
    if n:find("mining", 1, true) or n:find("minage", 1, true) then
      if not throttle("GatheringMine", 1.0) then TrySpeak("GatheringMine") end; return
    end
    if n:find("herb", 1, true) or n:find("herbor", 1, true) then
      if not throttle("GatheringHerb", 1.0) then TrySpeak("GatheringHerb") end; return
    end
    if n:find("skinn", 1, true) or n:find("depec", 1, true) or n:find("dépeç", 1, true) then
      if not throttle("GatheringSkin", 1.0) then TrySpeak("GatheringSkin") end; return
    end
    if n:find("cuis", 1, true) or n:find("cook", 1, true) then
      if not throttle("CookingStarted", 2.0) then TrySpeak("CookingStarted") end
    end

    -- Hearth / Healthstone / Soulstone / Portails / Potion / Démon
    local function any(t) return matchAny(n, t) end
    if any(KW.hearthstone) then if not throttle("hearth", 1.5) then TrySpeak("HearthstoneUsed") end; return end
    if any(KW.healthstone) then if not throttle("healthstone", 1.5) then TrySpeak("HealthstoneUsed") end; return end
    if any(KW.soulstone)  then if not throttle("soulstone", 1.5) then TrySpeak("SoulstoneApplied") end; return end
    if any(KW.summon_rit) then if not throttle("summon_ritual", 1.5) then TrySpeak("SummonStoneUsed") end; return end
    if any(KW.gateway)    then if not throttle("gateway", 1.5) then TrySpeak("PortalUsed") end; return end
    if any(KW.potion)     then if not throttle("potion", 1.5) then TrySpeak("PotionUsed") end; return end
    if any(KW.sum_demon)  then
      local gate = (event == "UNIT_SPELLCAST_START") and "sum_demon_start" or "sum_demon_succeeded"
      if not throttle(gate, 1.0) then TrySpeak("DemonSummoned"); TrySpeak("PetResummoned") end
      return
    end

    -- ===== SpellCastMajor (après tout le reste pour éviter les doublons) =====
    if event == "UNIT_SPELLCAST_START" then
      if isMajorStart(spellID, spellName) then
        FireSpellCastMajor()
        return
      end
    else -- SUCCEEDED
      if isMajorSucc(spellID, spellName) then
        FireSpellCastMajor()
        return
      end
    end

  -- ===== Group / Raid =====
  elseif event == "PARTY_INVITE_REQUEST" or event == "GROUP_INVITE_CONFIRMATION" then
    TrySpeak("GroupInvite")

  elseif event == "GROUP_ROSTER_UPDATE" then
    local inGroup = IsInGroup() or IsInRaid()
    if inGroup and not wasInGroup then TrySpeak("GroupJoined")
    elseif (not inGroup) and wasInGroup then TrySpeak("GroupLeft") end
    wasInGroup = inGroup

  elseif event == "READY_CHECK" then
    TrySpeak("ReadyCheck")

  -- ===== Craft/Quest/LFD =====
  elseif event == "TRADE_SKILL_SHOW" then
    TrySpeak("CraftingStarted")

  elseif event == "CHAT_MSG_SYSTEM" then
    local msg = ...
    local l = norm(msg or "")
    if l:find("you create", 1, true) or l:find("vous cr", 1, true) then
      if not throttle("CraftingCompleted", 1.0) then TrySpeak("CraftingCompleted") end
    end

  elseif event == "QUEST_TURNED_IN" then
    Emit("QuestCompleted") -- délai 1.5s

  elseif event == "LFG_QUEUE_STATUS_UPDATE" then
    -- throttle 3min → modulé par dialogue_rate
    if not throttle("DungeonQueueJoin", 180, true) then TrySpeak("DungeonQueueJoin") end

  -- ===== Auras =====
  elseif event == "UNIT_AURA" then
    local unit = ...
    scanBuffsAndDetect(unit)

  -- ===== Mort du joueur (wipe potentiel) =====
  elseif event == "PLAYER_DEAD" then
    C_Timer.After(1.0, function()
      if IsFullGroupDead() then
        Emit("RaidWipe") -- sera joué plus tard si pet absent
      end
    end)

  -- ===== Pet (pour flush de la file si on revient d’une mort) =====
  elseif event == "UNIT_PET" then
    local unit = ...
    if unit == "player" then
      C_Timer.After(0.3, FlushPending)
    end
  end
end

-- ===== Register events =====
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("PLAYER_MOUNT_DISPLAY_CHANGED")
f:RegisterEvent("PLAYER_LEVEL_UP")

f:RegisterEvent("MERCHANT_SHOW")
f:RegisterEvent("MERCHANT_CLOSED")
f:RegisterEvent("PLAYER_MONEY")

f:RegisterEvent("BANKFRAME_OPENED")
f:RegisterEvent("GUILDBANKFRAME_OPENED")
f:RegisterEvent("MAIL_SHOW")
f:RegisterEvent("AUCTION_HOUSE_SHOW")
f:RegisterEvent("TRANSMOGRIFY_OPEN")
f:RegisterEvent("TRAINER_SHOW")

f:RegisterEvent("PLAYER_UPDATE_RESTING")
f:RegisterEvent("ENCOUNTER_END")

f:RegisterEvent("ZONE_CHANGED")
f:RegisterEvent("ZONE_CHANGED_NEW_AREA")
f:RegisterEvent("ZONE_CHANGED_INDOORS")

f:RegisterEvent("CHAT_MSG_TEXT_EMOTE")
f:RegisterEvent("CHAT_MSG_EMOTE")
f:RegisterEvent("CHAT_MSG_LOOT")
f:RegisterEvent("CHAT_MSG_SYSTEM")

f:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
f:RegisterEvent("LOOT_OPENED")

f:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")   -- pêche & eat/drink (canal)
f:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")  -- pêche: vérif différée
f:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
f:RegisterEvent("UNIT_SPELLCAST_START")
f:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")

f:RegisterEvent("PARTY_INVITE_REQUEST")
f:RegisterEvent("GROUP_INVITE_CONFIRMATION")
f:RegisterEvent("GROUP_ROSTER_UPDATE")
f:RegisterEvent("READY_CHECK")

f:RegisterEvent("TRADE_SKILL_SHOW")
f:RegisterEvent("QUEST_TURNED_IN")
f:RegisterEvent("LFG_QUEUE_STATUS_UPDATE")

f:RegisterEvent("UNIT_AURA")

f:RegisterEvent("PLAYER_DEAD")
f:RegisterEvent("UNIT_PET")

f:SetScript("OnEvent", OnEvent)

-- ====== NEW: Hook public de test pour l'UI Options ==========================
function SRPWL.TestRandomUtterance()
  local keys = { "imp", "voidwalker", "succubus", "sayaad", "felhunter", "felguard" }
  local k = keys[ math.random(#keys) ]

  local samples = {
    "Awaiting orders...",
    "Shadows heed my call.",
    "Your will, my command.",
    "The fel is restless...",
    "I hunger for battle.",
    "Another soul to tease..."
  }
  local line = samples[ math.random(#samples) ]

  local actions = { "Preview", "Test", "Demo" }
  local act = actions[ math.random(#actions) ]

  if SRPWL and SRPWL.ShowTalking then
    SRPWL.ShowTalking(k, nil, act, line, nil, (DB.talking_duration or 4.5))
  end
end
