-- =============================================================================
-- KillLog.lua — self-maintained boss-kill log for legacy raids
-- =============================================================================
-- The game reports per-boss lockout state for modern raids (numEncounters > 0),
-- but TBC/Wrath raids come back with numEncounters == 0 — only an instance-level
-- "locked" flag, no record of WHICH bosses you've cleared. The only way to know
-- is to watch the kills ourselves (the same thing DBM-style addons do) and
-- persist them against the current lockout.
--
-- We record from three signals, most-to-least precise:
--   * ENCOUNTER_END (success == 1)  — encounter journal completion
--   * BOSS_KILL                     — encounter journal kill
--   * COMBAT_LOG_EVENT_UNFILTERED   — UNIT_DIED of a known boss npc (the
--                                     fallback for legacy bosses that don't fire
--                                     the journal events). Registered ONLY while
--                                     inside a raid, so it costs nothing at large.
--
-- Each instance's log carries the absolute reset time, so it auto-expires on the
-- weekly reset (with a 7-day hard fallback if we never learned the reset).
-- AvailabilityService consults this for legacy raids to turn an honest "?" into a
-- confident "defeated" once we've actually witnessed the kill.
-- =============================================================================

local ADDON, NS = ...
local ETB = NS.ETB

ETB.KillLog = ETB.KillLog or {}
local KL = ETB.KillLog

local WEEK = 7 * 24 * 60 * 60

local function now()
    if GetServerTime then return GetServerTime() end
    return time()
end

-- Tolerant name key (matches Lockouts / AvailabilityService normalization).
local function nkey(name)
    if type(name) ~= "string" then return nil end
    return (name:lower():gsub("[^%w]", ""))
end

local function GetNPCID(guid)
    if not guid then return nil end
    local t, _, _, _, _, npcID = strsplit("-", guid)
    if t == "Creature" or t == "Vehicle" then return tonumber(npcID) end
    return nil
end

-- SavedVariablesPerCharacter table (declared in the TOC). Lazily ensured.
local function db()
    if type(ETB_KillLog) ~= "table" then ETB_KillLog = {} end
    return ETB_KillLog
end

-- Absolute reset time for an instance, pulled from the live lockout cache when
-- available (the kill itself triggers a lockout refresh moments later).
local function lockoutResetFor(instName)
    if ETB.Lockouts and ETB.Lockouts.GetInstanceRecords then
        local recs = ETB.Lockouts:GetInstanceRecords(instName)
        if recs then
            for _, r in ipairs(recs) do
                if r.resetAt then return r.resetAt end
            end
        end
    end
    return nil
end

-- Effective expiry for an entry: the real reset if known, else a week from the
-- last kill (a safe upper bound — raid lockouts are weekly).
local function expiryOf(entry)
    return entry.resetAt or ((entry.lastKill or 0) + WEEK)
end

-- Drop an instance log if its lockout has reset.
local function pruneIfExpired(key, entry)
    if now() > expiryOf(entry) then
        db()[key] = nil
        return true
    end
    return false
end

-- Current raid instance name, or nil if not in a raid.
local function currentRaidName()
    if not IsInInstance then return nil end
    local inInstance, instType = IsInInstance()
    if not inInstance or instType ~= "raid" then return nil end
    local name = GetInstanceInfo and GetInstanceInfo() or nil
    if type(name) == "string" and name ~= "" then return name end
    return nil
end

-- ---------------------------------------------------------------------------
-- Recording
-- ---------------------------------------------------------------------------

-- Record a boss kill in the current raid. encID and/or bossName may be given.
function KL:Record(encID, bossName)
    if not encID and not bossName then return end
    local instName = currentRaidName()
    if not instName then return end

    local key = nkey(instName)
    if not key then return end

    local store = db()
    local entry = store[key]
    if not entry then
        entry = { name = instName, killedBosses = {}, killedEnc = {} }
        store[key] = entry
    end

    local kt = now()
    entry.lastKill = kt
    local resetAt = lockoutResetFor(instName)
    if resetAt then entry.resetAt = resetAt end

    if encID then entry.killedEnc[encID] = kt end
    if bossName then
        local bk = nkey(bossName)
        if bk then entry.killedBosses[bk] = kt end
    end
end

-- ---------------------------------------------------------------------------
-- Queries (consumed by AvailabilityService)
-- ---------------------------------------------------------------------------

-- Returns:
--   true   — we logged this boss killed this lockout (confident)
--   false  — we have a log for this instance this lockout but NOT this boss
--            (we were tracking and didn't see it die — likely still up)
--   nil    — no log for this instance this lockout (we can't say)
function KL:WasKilledThisLockout(instName, encNameOrID)
    if not instName then return nil end
    local key = nkey(instName)
    if not key then return nil end

    local entry = db()[key]
    if not entry then return nil end
    if pruneIfExpired(key, entry) then return nil end

    if type(encNameOrID) == "number" then
        if entry.killedEnc[encNameOrID] then return true end
    elseif type(encNameOrID) == "string" then
        local bk = nkey(encNameOrID)
        if bk and entry.killedBosses[bk] then return true end
    end
    -- We have a (live) log for this instance but not this boss.
    return false
end

-- Has ETB logged ANY kill for this instance this lockout? (i.e. were we tracking)
function KL:HasInstanceLog(instName)
    if not instName then return false end
    local key = nkey(instName)
    if not key then return false end
    local entry = db()[key]
    if not entry then return false end
    if pruneIfExpired(key, entry) then return false end
    return next(entry.killedBosses) ~= nil or next(entry.killedEnc) ~= nil
end

-- Prune all expired logs (called on login).
function KL:PruneExpired()
    local store = db()
    for key, entry in pairs(store) do
        if now() > expiryOf(entry) then store[key] = nil end
    end
end

-- Plain snapshot for /etb killlog.
function KL:Dump()
    local out = {}
    for key, entry in pairs(db()) do
        local bosses = {}
        for bk in pairs(entry.killedBosses) do bosses[#bosses + 1] = bk end
        out[#out + 1] = {
            name = entry.name or key,
            resetAt = entry.resetAt,
            lastKill = entry.lastKill,
            bosses = bosses,
        }
    end
    return out
end

-- ---------------------------------------------------------------------------
-- Wiring
-- ---------------------------------------------------------------------------

function KL:Init()
    if self._inited then return end
    self._inited = true

    local EB = ETB.EventBus
    if EB then
        -- Prune expired logs once SavedVariables are guaranteed loaded.
        EB:Register("PLAYER_LOGIN", function() KL:PruneExpired() end)
        -- Encounter-journal signals (precise; fire for modern content, and may
        -- fire for some legacy content too — harmless either way).
        EB:Register("ENCOUNTER_END", function(encounterID, encounterName, _, _, success)
            if success == 1 or success == true then
                KL:Record(encounterID, encounterName)
            end
        end)
        EB:Register("BOSS_KILL", function(encounterID, name)
            KL:Record(encounterID, name)
        end)
    end

    -- Combat-log fallback for legacy bosses that don't fire journal events.
    -- Registered ONLY while inside a raid so it never runs during normal play.
    local clf = CreateFrame("Frame")
    clf:SetScript("OnEvent", function()
        local _, sub, _, _, _, _, _, destGUID, destName = CombatLogGetCurrentEventInfo()
        if sub == "UNIT_DIED" or sub == "PARTY_KILL" then
            local npcID = GetNPCID(destGUID)
            if npcID and ETB.JNpcToEnc then
                local encID = ETB.JNpcToEnc[npcID]
                -- Only known bosses have an encounter mapping; trash is ignored.
                if encID then KL:Record(encID, destName) end
            end
        end
    end)

    local function updateReg()
        local inInstance, instType = IsInInstance()
        if inInstance and instType == "raid" then
            clf:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        else
            clf:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        end
    end
    if EB then
        EB:Register("PLAYER_ENTERING_WORLD", updateReg)
        EB:Register("ZONE_CHANGED_NEW_AREA", updateReg)
    end
    self._clf = clf
end

KL:Init()

ETB:Debug("KillLog", "Kill log loaded")

return KL
