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

-- ============================================================
-- 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

-- =======================================
-- 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")
f:RegisterEvent("CHAT_MSG_LOOT")
f:RegisterEvent("CHAT_MSG_SYSTEM")

-- 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.).

f:SetScript("OnEvent", function(self, event, msg)
  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

  -- Toys (identity: itemID)
  if not didPopup and isTracked and IsTrackedToy(itemID) and C_ToyBox and C_ToyBox.GetToyInfo and PlayerHasToy then
    local toyName = C_ToyBox.GetToyInfo(itemID)
    if toyName and PlayerHasToy(itemID) then
      local key = "toy:" .. tostring(itemID)
      if not Seen(key) then
        MarkSeen(key)
        didPopup = true
        if ns and ns.ShowNewCollectionPopup then
          ns.ShowNewCollectionPopup(itemName or toyName, icon, "Toy", { type = "toy", itemID = itemID, itemLink = link, isNew = true })
        end
      end
    end
  end

  -- Mount items (identity: mountID; mapping cached in ns._itemToMountID)
  if not didPopup and isTracked and ns and ns.IsMountItemCollected and ns.IsMountItemCollected(itemID) then
    local mountID = ns._itemToMountID and ns._itemToMountID[itemID] or nil
    if mountID and IsTrackedMount(mountID) then
      local key = "mount:" .. tostring(mountID)
      if not Seen(key) then
        MarkSeen(key)
        didPopup = true
        if ns and ns.ShowNewCollectionPopup then
          ns.ShowNewCollectionPopup(itemName or "New mount", icon, "Mount", { type = "mount", mountID = mountID, isNew = true })
        end
      end
    end
  end

  -- Pet items (identity: speciesID)
  if not didPopup and isTracked and C_PetJournal and C_PetJournal.GetPetInfoByItemID and ns and ns.IsPetItemCollected and ns.IsPetItemCollected(itemID) then
    local ok, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13 = pcall(C_PetJournal.GetPetInfoByItemID, itemID)
    local speciesID = nil
    if ok then
      if type(r1) == "table" then
        speciesID = tonumber(r1.speciesID or r1[13] or r1[2])
      else
        speciesID = tonumber(r13) or tonumber(r2)
      end
    end

    if speciesID and IsTrackedPet(speciesID) then
      local key = "pet:" .. tostring(speciesID)
      if not Seen(key) then
        MarkSeen(key)
        didPopup = true
        if ns and ns.ShowNewCollectionPopup then
          ns.ShowNewCollectionPopup(itemName or "New pet", icon, "Pet", { type = "pet", speciesID = speciesID, isNew = true })
        end
      end
    end
  end

  -- Tracked Items (raid/dungeon loot etc.)
  -- Show only on the first time this character obtains the item, and only if the item exists in Collection Log.
  if not didPopup and isTracked then
    ns.EnsureCharacterRecord()
    local guid = ns.GetPlayerGUID()
    local c = guid and CollectionLogDB.characters and CollectionLogDB.characters[guid] or nil
    local already = (c and c.items and c.items[itemID] and (c.items[itemID].count or 0) > 0) and true or false

    if not already then
      local key = "item:" .. tostring(itemID)
      if not Seen(key) then
        MarkSeen(key)
        if ns and ns.ShowNewCollectionPopup then
          ns.ShowNewCollectionPopup(itemName or "New item", icon, "Item", { type = "item", itemID = itemID, itemLink = link, isNew = true })
        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")

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)
  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:SetScript("OnEvent", function(_, event, ...)
  if event == "PLAYER_ENTERING_WORLD" then
    UpdateRunContext()
    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
