-- CollectionLog_Track.lua
local ADDON, ns = ...

-- =======================================
-- Popup Debug (opt-in)
-- =======================================
local function PopupDebugLog(line)
  CollectionLogDB = CollectionLogDB or {}
  CollectionLogDB.debug = CollectionLogDB.debug or {}
  CollectionLogDB.debug.popup = CollectionLogDB.debug.popup or { enabled = false, lines = {} }
  local d = CollectionLogDB.debug.popup
  if not d.enabled then return end
  local t = date("%H:%M:%S")
  local msg = "[CL PopDebug " .. t .. "] " .. tostring(line)
  table.insert(d.lines, 1, msg)
  if #d.lines > 40 then
    table.remove(d.lines)
  end
  if ns and ns.Print then
    ns.Print(msg)
  else
    print(msg)
  end
end

ns.PopupDebugLog = PopupDebugLog

-- ============================================================
-- Pass 2: Real-time "New Collection" triggers (Blizzard-truthful)
--
-- Goals:
--   * Fire popup for Mounts / Pets / Toys / tracked Items (raid/dungeon loot)
--   * ONLY when the collectible exists in Collection Log data (no noise)
--   * Avoid heuristics: confirm collection via Blizzard APIs and/or your
--     existing CollectionLog item record state.
-- ============================================================

-- =======================================
-- Helpers
-- =======================================
local function GetItemIDFromLink(link)
  if not link then return nil end
  -- Works for item links and some battlepet links; we only care about items for now.
  local itemID = link:match("item:(%d+)")
  if itemID then return tonumber(itemID) end

  -- Fallback: try GetItemInfoInstant on the link (safe)
  local id = GetItemInfoInstant(link)
  if id then return id end
  return nil
end

local function GetQuantityFromLootMsg(msg)
  -- Many loot messages end with "xN"
  local qty = msg:match("x(%d+)")
  if qty then return tonumber(qty) end
  return 1
end

-- =======================================
-- Loot message filtering
--
-- CHAT_MSG_LOOT includes other players' loot messages ("X receives loot: ...").
-- We only want to record items the *player* obtained.
--
-- We do this using Blizzard's own localized global strings for self-loot
-- patterns, so we don't hardcode English phrasing.
-- =======================================
local _selfLootPatterns

local function _MakeLootPattern(fmt)
  if type(fmt) ~= "string" or fmt == "" then return nil end

  -- Replace format tokens with sentinels, escape Lua pattern characters,
  -- then expand sentinels into permissive patterns.
  local p = fmt
  p = p:gsub("%%s", "\001S\001")
  p = p:gsub("%%d", "\001D\001")
  p = p:gsub("([%(%)%.%+%-%*%?%[%]%^%$])", "%%%1")
  p = p:gsub("\001S\001", ".+")
  p = p:gsub("\001D\001", "%%d+")

  return "^" .. p .. "$"
end

local function _BuildSelfLootPatterns()
  if _selfLootPatterns then return end
  _selfLootPatterns = {}

  -- Common self-loot patterns (localized): loot, pushed loot, multiple stacks.
  local candidates = {
    _G.LOOT_ITEM_SELF,
    _G.LOOT_ITEM_SELF_MULTIPLE,
    _G.LOOT_ITEM_PUSHED_SELF,
    _G.LOOT_ITEM_PUSHED_SELF_MULTIPLE,
  }

  for _, fmt in ipairs(candidates) do
    local pat = _MakeLootPattern(fmt)
    if pat then
      table.insert(_selfLootPatterns, pat)
    end
  end
end

local function IsLootMessageForPlayer(event, msg)
  if event ~= "CHAT_MSG_LOOT" then return true end
  if type(msg) ~= "string" or msg == "" then return false end

  _BuildSelfLootPatterns()
  if _selfLootPatterns then
    for _, pat in ipairs(_selfLootPatterns) do
      if msg:match(pat) then
        return true
      end
    end
  end

  -- Fallback (rare clients / missing globals): only accept explicit "You..." lines.
  -- This is intentionally conservative to prevent false positives.
  return msg:match("^You ") ~= nil
end

-- ============================================================
-- Tracked lookup caches
--
-- We only want to trigger "New Collection" for collectibles that
-- exist inside Collection Log's current group index.
--
-- These caches are rebuilt opportunistically (on first use and whenever
-- ns.RebuildGroupIndex() runs).
-- ============================================================
local function RebuildTrackedCaches()
  ns._tracked = ns._tracked or {}
  ns._tracked.items = {}
  ns._tracked.mounts = {}
  ns._tracked.pets = {}
  ns._tracked.toys = {}

  if not (ns.Data and ns.Data.groups) then return end

  for _, g in pairs(ns.Data.groups) do
    if type(g) == "table" then
      if type(g.items) == "table" then
        for _, itemID in ipairs(g.items) do
          if type(itemID) == "number" and itemID > 0 then
            ns._tracked.items[itemID] = true
          end
        end
      end
      if type(g.mounts) == "table" then
        for _, mountID in ipairs(g.mounts) do
          if type(mountID) == "number" and mountID > 0 then
            ns._tracked.mounts[mountID] = true
          end
        end
      end
      if type(g.pets) == "table" then
        for _, speciesID in ipairs(g.pets) do
          if type(speciesID) == "number" and speciesID > 0 then
            ns._tracked.pets[speciesID] = true
          end
        end
      end
    end
  end

  -- Toys are identity itemID but we keep a dedicated flag for clarity.
  -- If the Toys importer is loaded, toys are already included in items.
  if ns._tracked.items then
    for itemID in pairs(ns._tracked.items) do
      if C_ToyBox and C_ToyBox.GetToyInfo and C_ToyBox.GetToyInfo(itemID) then
        ns._tracked.toys[itemID] = true
      end
    end
  end
end

local function EnsureTrackedCaches()
  if not (ns._tracked and ns._tracked.items) then
    RebuildTrackedCaches()
  end
end

-- Rebuild tracked caches any time the addon rebuilds its group index.
if hooksecurefunc and ns and ns.RebuildGroupIndex then
  pcall(hooksecurefunc, ns, "RebuildGroupIndex", function()
    RebuildTrackedCaches()
  end)
end

local function IsTrackedItem(itemID)
  if not itemID then return false end
  EnsureTrackedCaches()
  return ns._tracked and ns._tracked.items and ns._tracked.items[itemID] or false
end

local function IsTrackedToy(itemID)
  if not itemID then return false end
  EnsureTrackedCaches()
  return ns._tracked and ns._tracked.toys and ns._tracked.toys[itemID] or false
end

local function IsTrackedMount(mountID)
  if not mountID then return false end
  EnsureTrackedCaches()
  return ns._tracked and ns._tracked.mounts and ns._tracked.mounts[mountID] or false
end

local function IsTrackedPet(speciesID)
  if not speciesID then return false end
  EnsureTrackedCaches()
  return ns._tracked and ns._tracked.pets and ns._tracked.pets[speciesID] or false
end

-- =======================================
-- Identify conversion items (mount/pet/toy)
-- =======================================
local function IsToyItem(itemID)
  if not itemID or itemID <= 0 then return false end
  if C_ToyBox and C_ToyBox.GetToyInfo then
    local ok, name = pcall(C_ToyBox.GetToyInfo, itemID)
    if ok and name then return true end
  end
  if C_ToyBox and C_ToyBox.GetToyFromItemID then
    local ok, toyID = pcall(C_ToyBox.GetToyFromItemID, itemID)
    if ok and toyID then return true end
  end
  return false
end

local function GetMountIDFromItemID_Safe(itemID)
  if not itemID or itemID <= 0 then return nil end
  if ns and ns.GetMountIDFromItemID then
    local ok, mid = pcall(ns.GetMountIDFromItemID, itemID)
    if ok and tonumber(mid) and tonumber(mid) > 0 then
      return tonumber(mid)
    end
  end
  if C_MountJournal and C_MountJournal.GetMountFromItem then
    local ok, mid = pcall(C_MountJournal.GetMountFromItem, itemID)
    if ok and tonumber(mid) and tonumber(mid) > 0 then
      return tonumber(mid)
    end
  end
  return nil
end

local function GetSpeciesIDFromItemID_Safe(itemID)
  if not itemID or itemID <= 0 then return nil end
  if C_PetJournal and C_PetJournal.GetPetInfoByItemID then
    local ok, speciesID = pcall(C_PetJournal.GetPetInfoByItemID, itemID)
    if ok and tonumber(speciesID) and tonumber(speciesID) > 0 then
      return tonumber(speciesID)
    end
  end
  return nil
end

-- =======================================
-- Recording
-- =======================================
function ns.RecordObtained(itemID, quantity, source)
  if not itemID or itemID <= 0 then return end
  if not IsTrackedItem(itemID) then return end

  ns.EnsureCharacterRecord()
  local guid = ns.GetPlayerGUID()
  if not guid then return end

  quantity = quantity or 1
  if quantity < 1 then quantity = 1 end

  local c = CollectionLogDB.characters[guid]
  c.items = c.items or {}

  local rec = c.items[itemID]
  local now = time()

  if not rec then
    rec = { count = 0, firstTime = now, lastTime = now, source = source }
    c.items[itemID] = rec
  end

  rec.count = (rec.count or 0) + quantity
  rec.lastTime = now
  if not rec.firstTime then rec.firstTime = now end
  if source then rec.source = source end

  -- Light cache (optional): itemOwners
  CollectionLogDB.itemOwners[itemID] = CollectionLogDB.itemOwners[itemID] or {}
  CollectionLogDB.itemOwners[itemID][guid] = true

  -- If UI is open, refresh grid (cheap enough for MVP)
  if ns.UI and ns.UI.frame and ns.UI.frame:IsShown() then
    ns.UI.RefreshGrid()
  end
end

-- =======================================
-- Loot/Purchase detection (chat-based)
-- =======================================
local f = CreateFrame("Frame")

-- Disable chat-based tracking in Challenge Mode / Mythic+ to avoid taint.
local function IsChallengeModeActiveSafe()
  if C_ChallengeMode and C_ChallengeMode.IsChallengeModeActive then
    local ok, active = pcall(C_ChallengeMode.IsChallengeModeActive)
    if ok and active then return true end
  end
  return false
end

local function SetChatTrackingEnabled(enabled)
  if not f then return end
  f:UnregisterAllEvents()
  if enabled then
  end
end

-- NOTE:
-- CHAT_MSG_LOOT is the best single “OSRS-like” source: it fires for looting and many purchases.
-- We’ll expand later for edge cases (mailbox, quest rewards, crafting, etc.).

SetChatTrackingEnabled(not IsChallengeModeActiveSafe())


f:SetScript("OnEvent", function(self, event, ...)
  local msg = select(1, ...)
  if not msg or type(msg) ~= "string" then return end

  -- CHAT_MSG_LOOT includes loot lines for other players in your group.
  -- Only accept messages that correspond to the player obtaining the item.
  if not IsLootMessageForPlayer(event, msg) then return end

  -- Extract item link (first link in the message)
  local link = msg:match("(|c%x%x%x%x%x%x%x%x|Hitem:.-|h%[.-%]|h|r)")
  if not link then
    -- Sometimes links appear without color codes; catch raw |Hitem:
    link = msg:match("(|Hitem:.-|h%[.-%]|h)")
  end
  if not link then return end

  local itemID = GetItemIDFromLink(link)
  if not itemID then return end

  local qty = GetQuantityFromLootMsg(msg)

  -- Only react to items that are in Collection Log.
  -- We still record counts (below) for tracked items only.
  local isTracked = IsTrackedItem(itemID)

  -- OSRS-style popup when a *new* collectible is learned/collected
  CollectionLogDB = CollectionLogDB or {}
  CollectionLogDB._seenNewPopups = CollectionLogDB._seenNewPopups or {}

  local function MarkSeen(key)
    CollectionLogDB._seenNewPopups[key] = true
  end
  local function Seen(key)
    return CollectionLogDB._seenNewPopups[key] and true or false
  end

  local itemName = (C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(itemID)) or (GetItemInfo and GetItemInfo(itemID))
  local icon = (C_Item and C_Item.GetItemIconByID and C_Item.GetItemIconByID(itemID)) or (select(10, GetItemInfo(itemID)))

  local didPopup = false

  -- Tracked Items (raid/dungeon loot etc.)
  -- Popup should ONLY fire when the *account* gains a new appearance from this item.
  -- IMPORTANT: do NOT treat conversion collectibles (toy/mount/pet items) as transmog.
  if not didPopup and isTracked and ns and ns.HasCollectedAppearance then
    -- Skip conversion items entirely (they are handled by NEW_TOY_ADDED / NEW_MOUNT_ADDED / NEW_PET_ADDED).
    local isToy = IsToyItem(itemID)
    local mountFromItem = GetMountIDFromItemID_Safe(itemID)
    local speciesFromItem = GetSpeciesIDFromItemID_Safe(itemID)
    if isToy or mountFromItem or speciesFromItem then
      PopupDebugLog("Skip appearance check for conversion item itemID=" .. itemID .. " toy=" .. tostring(isToy) .. " mountID=" .. tostring(mountFromItem) .. " speciesID=" .. tostring(speciesFromItem))
    else
      local wasCollected = ns.HasCollectedAppearance(itemID) == true
      PopupDebugLog("Loot itemID=" .. itemID .. " wasCollected=" .. tostring(wasCollected))
      if not wasCollected then
        -- Some unlocks are delayed (especially mass-loot / BoE vendor / cache misses).
        -- Try a small fixed set of retries (NO loops): 0.35s, 0.9s, 1.8s
        CollectionLogDB = CollectionLogDB or {}
        CollectionLogDB._pendingAppearanceChecks = CollectionLogDB._pendingAppearanceChecks or {}
        local token = (CollectionLogDB._pendingAppearanceChecks[itemID] or 0) + 1
        CollectionLogDB._pendingAppearanceChecks[itemID] = token

        local function Attempt(attemptN)
          if not (ns and ns.HasCollectedAppearance) then return end
          -- If a newer loot line for the same itemID arrived, abandon this attempt.
          if CollectionLogDB._pendingAppearanceChecks[itemID] ~= token then return end

          local nowCollected = ns.HasCollectedAppearance(itemID) == true
          PopupDebugLog("Appearance attempt " .. attemptN .. " itemID=" .. itemID .. " nowCollected=" .. tostring(nowCollected))
          if not nowCollected then return false end

          CollectionLogDB._seenNewPopups = CollectionLogDB._seenNewPopups or {}
          local key = "appearance_item:" .. tostring(itemID)
          if CollectionLogDB._seenNewPopups[key] then
            PopupDebugLog("Already seen popup key=" .. key)
            return true
          end
          CollectionLogDB._seenNewPopups[key] = true

          local itemName2 = (C_Item and C_Item.GetItemNameByID and C_Item.GetItemNameByID(itemID)) or (GetItemInfo and GetItemInfo(itemID)) or "New appearance"
          local icon2 = (C_Item and C_Item.GetItemIconByID and C_Item.GetItemIconByID(itemID)) or (select(10, GetItemInfo(itemID)))
          if ns and ns.ShowNewCollectionPopup then
            ns.ShowNewCollectionPopup(itemName2, icon2, "Transmog", { type = "appearance", itemID = itemID, itemLink = link, isNew = true, source = "appearance_flip" })
            PopupDebugLog("Fired appearance popup itemID=" .. itemID)
          end
          return true
        end

        local delays = { 0.35, 0.90, 1.80 }
        for i, d in ipairs(delays) do
          C_Timer.After(d, function()
            -- Attempt; if it succeeds we don't need later ones.
            if Attempt(i) then
              -- Invalidate token so later timers no-op.
              if CollectionLogDB and CollectionLogDB._pendingAppearanceChecks then
                CollectionLogDB._pendingAppearanceChecks[itemID] = -1
              end
            end
          end)
        end
      end
    end
  end

  -- Source tagging (optional): rough labels
  -- Later we can detect instance/encounter/vendor specifics.
  local source = "obtained"

  if isTracked then
    ns.RecordObtained(itemID, qty, source)
  end
end)

-- =======================================
-- Mount + Pet journal events
-- =======================================
local jf = CreateFrame("Frame")
jf:RegisterEvent("NEW_MOUNT_ADDED")
jf:RegisterEvent("NEW_PET_ADDED")
jf:RegisterEvent("NEW_TOY_ADDED")

local function SafeMountPopup(mountID)
  if not mountID or mountID <= 0 then return end
  if not IsTrackedMount(mountID) then return end

  CollectionLogDB = CollectionLogDB or {}
  CollectionLogDB._seenNewPopups = CollectionLogDB._seenNewPopups or {}
  local key = "mount:" .. tostring(mountID)
  if CollectionLogDB._seenNewPopups[key] then return end
  CollectionLogDB._seenNewPopups[key] = true

  if not (C_MountJournal and C_MountJournal.GetMountInfoByID) then return end
  local ok, name, spellID, icon, _, _, _, _, _, _, isCollected = pcall(C_MountJournal.GetMountInfoByID, mountID)
  if not ok then return end
  if not isCollected then return end

  if ns and ns.ShowNewCollectionPopup then
    ns.ShowNewCollectionPopup(name or "New mount", icon, "Mount", { type = "mount", mountID = mountID, spellID = spellID, isNew = true })
  end
end

local function SafePetPopup(petGUID)
  if not petGUID or petGUID == "" then return end
  if not (C_PetJournal and C_PetJournal.GetPetInfoByPetID and C_PetJournal.GetPetInfoBySpeciesID) then return end

  local ok, name, speciesID = pcall(C_PetJournal.GetPetInfoByPetID, petGUID)
  if not ok then return end
  speciesID = tonumber(speciesID)
  if not speciesID or speciesID <= 0 then return end
  if not IsTrackedPet(speciesID) then return end

  CollectionLogDB = CollectionLogDB or {}
  CollectionLogDB._seenNewPopups = CollectionLogDB._seenNewPopups or {}
  local key = "pet:" .. tostring(speciesID)
  if CollectionLogDB._seenNewPopups[key] then return end
  CollectionLogDB._seenNewPopups[key] = true

  local ok2, sname, _, _, _, _, _, _, icon = pcall(C_PetJournal.GetPetInfoBySpeciesID, speciesID)
  if ok2 and sname and sname ~= "" then name = sname end

  if ns and ns.ShowNewCollectionPopup then
    ns.ShowNewCollectionPopup(name or "New pet", icon, "Pet", { type = "pet", speciesID = speciesID, isNew = true })
  end
end

jf:SetScript("OnEvent", function(_, event, arg1)
  if event == "NEW_MOUNT_ADDED" then
    SafeMountPopup(tonumber(arg1))
  elseif event == "NEW_PET_ADDED" then
    SafePetPopup(arg1)
  elseif event == "NEW_TOY_ADDED" then
    local itemID = tonumber(arg1)
    if itemID and itemID > 0 and PlayerHasToy and PlayerHasToy(itemID) and IsTrackedToy(itemID) then
      CollectionLogDB = CollectionLogDB or {}
      CollectionLogDB._seenNewPopups = CollectionLogDB._seenNewPopups or {}
      local key = "toy:" .. tostring(itemID)
      if not CollectionLogDB._seenNewPopups[key] then
        CollectionLogDB._seenNewPopups[key] = true
        local name = (C_ToyBox and C_ToyBox.GetToyInfo and C_ToyBox.GetToyInfo(itemID)) or (GetItemInfo and GetItemInfo(itemID)) or "New toy"
        local icon = (C_Item and C_Item.GetItemIconByID and C_Item.GetItemIconByID(itemID)) or (select(10, GetItemInfo(itemID)))
        if ns and ns.ShowNewCollectionPopup then
          ns.ShowNewCollectionPopup(name, icon, "Toy", { type = "toy", itemID = itemID, isNew = true })
        end
      end
    end
  end
end)

-- =======================================
-- Optional: manual test command
-- =======================================

-- ============================================================
-- Instance Completion Counter (OSRS-style "KC")
-- ============================================================
-- Definition: a "completion" increments when the player has killed ALL encounters
-- in an instance for the current difficulty within a single continuous run.
--
-- This is Blizzard-truthful: driven by ENCOUNTER_END success events + Encounter Journal encounter lists.
-- It is NOT retroactive (no universal lifetime API); it starts counting after install.

local function EnsureEJLoaded()
  if (EJ_GetEncounterInfoByIndex and EJ_SelectInstance and EJ_GetInstanceForEncounter) then
    return true
  end
  pcall(LoadAddOn, "Blizzard_EncounterJournal")
  if EncounterJournal_LoadUI then pcall(EncounterJournal_LoadUI) end
  return (EJ_GetEncounterInfoByIndex and EJ_SelectInstance) ~= nil
end

local function GetEncounterIDsForInstance(instanceID)
  if not instanceID then return nil end
  if not EnsureEJLoaded() then return nil end

  ns._encounterCache = ns._encounterCache or {}
  if ns._encounterCache[instanceID] then
    return ns._encounterCache[instanceID]
  end

  local list = {}
  pcall(EJ_SelectInstance, instanceID)

  local i = 1
  while true do
    local ok, _, _, encID = pcall(EJ_GetEncounterInfoByIndex, i)
    if not ok or not encID then break end
    list[#list + 1] = tonumber(encID)
    i = i + 1
  end

  ns._encounterCache[instanceID] = list
  return list
end

local function GetActiveRunKey(instanceID, difficultyID)
  return tostring(instanceID or 0) .. ":" .. tostring(difficultyID or 0)
end

function ns.GetInstanceCompletionCount(instanceID, difficultyID)
  ns.EnsureCharacterRecord()
  local guid = ns.GetPlayerGUID()
  if not guid then return 0 end
  local c = CollectionLogDB.characters and CollectionLogDB.characters[guid]
  if not c then return 0 end
  c.completions = c.completions or {}
  c.completions[instanceID] = c.completions[instanceID] or {}
  return tonumber(c.completions[instanceID][difficultyID] or 0) or 0
end

local function IncrementInstanceCompletion(instanceID, difficultyID)
  ns.EnsureCharacterRecord()
  local guid = ns.GetPlayerGUID()
  if not guid then return end
  local c = CollectionLogDB.characters[guid]
  c.completions = c.completions or {}
  c.completions[instanceID] = c.completions[instanceID] or {}
  c.completions[instanceID][difficultyID] = (tonumber(c.completions[instanceID][difficultyID] or 0) or 0) + 1
end

-- Runtime-only tracking for the current run (cleared when you leave / enter a new instance).
ns._activeRun = ns._activeRun or { key = nil, killed = {}, completed = false }

local function ResetActiveRun()
  ns._activeRun.key = nil
  ns._activeRun.killed = {}
  ns._activeRun.completed = false
end

local function UpdateRunContext()
  local inInstance = IsInInstance()
  if not inInstance then
    ResetActiveRun()
    return
  end

  local name, instanceType, difficultyID, _, _, _, _, instanceID = GetInstanceInfo()
  instanceID = tonumber(instanceID)
  difficultyID = tonumber(difficultyID)

  if not instanceID or not difficultyID then
    ResetActiveRun()
    return
  end

  local key = GetActiveRunKey(instanceID, difficultyID)
  if ns._activeRun.key ~= key then
    ns._activeRun.key = key
    ns._activeRun.killed = {}
    ns._activeRun.completed = false
  end
end

local function MarkEncounterKilled(instanceID, difficultyID, encounterID)
  UpdateRunContext()
  if not ns._activeRun.key then return end

  local key = GetActiveRunKey(instanceID, difficultyID)
  if ns._activeRun.key ~= key then
    -- Context changed; ensure run context matches
    ns._activeRun.key = key
    ns._activeRun.killed = {}
    ns._activeRun.completed = false
  end

  ns._activeRun.killed[encounterID] = true

  -- Check completion against EJ's encounter list.
  local list = GetEncounterIDsForInstance(instanceID)
  if not list or #list == 0 then return end
  if ns._activeRun.completed then return end

  for _, eid in ipairs(list) do
    if not ns._activeRun.killed[eid] then
      return -- not complete yet
    end
  end

  -- All encounters killed in this run => completion!
  ns._activeRun.completed = true
  IncrementInstanceCompletion(instanceID, difficultyID)

  -- If UI open, refresh header counts immediately.
  if ns.UI and ns.UI.RefreshGrid then
    pcall(ns.UI.RefreshGrid)
  end
end

-- Event frame for encounter completion tracking
local kc = CreateFrame("Frame")
kc:RegisterEvent("PLAYER_ENTERING_WORLD")
kc:RegisterEvent("ENCOUNTER_END")
kc:RegisterEvent("CHALLENGE_MODE_RESET")
kc:RegisterEvent("CHALLENGE_MODE_COMPLETED")
kc:RegisterEvent("CHALLENGE_MODE_START")

kc:SetScript("OnEvent", function(_, event, ...)
  if event == "PLAYER_ENTERING_WORLD" then
    UpdateRunContext()
    SetChatTrackingEnabled(not IsChallengeModeActiveSafe())
    return
  end

  if event == "CHALLENGE_MODE_START" then
    SetChatTrackingEnabled(false)
    return
  end

  if event == "CHALLENGE_MODE_COMPLETED" or event == "CHALLENGE_MODE_RESET" then
    SetChatTrackingEnabled(true)
    return
  end

  if event == "ENCOUNTER_END" then
    local encounterID, _, difficultyID, _, success = ...
    if success ~= 1 then return end

    local _, _, instDifficultyID, _, _, _, _, instanceID = GetInstanceInfo()
    instanceID = tonumber(instanceID)
    instDifficultyID = tonumber(instDifficultyID)

    -- Prefer the instance difficulty from GetInstanceInfo (more reliable in some clients)
    local did = instDifficultyID or tonumber(difficultyID)
    if not instanceID or not did or not encounterID then return end

    MarkEncounterKilled(instanceID, did, tonumber(encounterID))
  end
end)

SLASH_COLLECTIONLOGTEST1 = "/clogadd"
SlashCmdList.COLLECTIONLOGTEST = function(msg)
  -- /clogadd 19019 2
  local a, b = msg:match("^(%d+)%s*(%d*)$")
  local itemID = tonumber(a)
  local qty = tonumber(b) or 1
  if itemID then
    ns.RecordObtained(itemID, qty, "manual")
    ns.Print(("Manually recorded item %d x%d"):format(itemID, qty))
  else
    ns.Print("Usage: /clogadd <itemID> [qty]")
  end
end


-- ============================================================
-- Pets helper (no scan; builds the Blizzard-truthful pets group)
-- ============================================================
SLASH_COLLECTIONLOGPETS1 = "/clogpets"
SlashCmdList.COLLECTIONLOGPETS = function()
  if ns and ns.EnsurePetsGroups then
    local g = ns.EnsurePetsGroups()
    if g and g.pets then
      DEFAULT_CHAT_FRAME:AddMessage(("Collection Log: Pets group built (%d species)."):format(#g.pets))
    else
      DEFAULT_CHAT_FRAME:AddMessage("Collection Log: Pet Journal not ready yet. Open your Pet Journal (Shift+P) once, then revisit the Pets tab.")
    end
  else
    DEFAULT_CHAT_FRAME:AddMessage("Collection Log: Pets function missing (file not loaded).")
  end
end
