-- =============================================================================
-- AltTracker.lua — account-wide character snapshots (Phase 1)
-- =============================================================================
-- An addon can only read the LOGGED-IN character's lockouts and kills live. To
-- show account-wide "which of my characters can still farm this / what has each
-- alt done this week", every character writes a snapshot of its state into an
-- ACCOUNT-WIDE SavedVariable (ETB_AltDB) on login and whenever lockouts refresh.
-- Other characters then read those snapshots.
--
-- Inherent limitation (true of every alt-tracker): an offline alt's data is only
-- as fresh as its last login. We store `lastSeen` so the UI can say so honestly.
--
-- This phase is the foundation: the store, the snapshot writer, and a query API.
-- The visible surfaces (wishlist drop-chances, weekly overview) build on this.
-- =============================================================================

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

ETB.AltTracker = ETB.AltTracker or {}
local AT = ETB.AltTracker

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

local function nkey(name)
    if type(name) ~= "string" then return nil end
    return (name:lower():gsub("[^%w]", ""))
end

-- Class -> the armor type that class wears/transmogs at max level.
local CLASS_ARMOR = {
    WARRIOR = "PLATE", PALADIN = "PLATE", DEATHKNIGHT = "PLATE",
    HUNTER = "MAIL", SHAMAN = "MAIL",
    ROGUE = "LEATHER", DRUID = "LEATHER", MONK = "LEATHER",
    MAGE = "CLOTH", WARLOCK = "CLOTH", PRIEST = "CLOTH",
}
function AT:ClassArmor(classToken)
    return classToken and CLASS_ARMOR[classToken] or nil
end

local function adb()
    if type(ETB_AltDB) ~= "table" then ETB_AltDB = {} end
    return ETB_AltDB
end

local function charKey()
    local name = UnitName("player")
    local realm = GetRealmName and GetRealmName() or ""
    if not name then return nil end
    return name .. "-" .. (realm or "")
end

-- Public: key of the currently logged-in character (for "(you)" labelling).
function AT:CurrentKey() return charKey() end

-- ---------------------------------------------------------------------------
-- Snapshot writer
-- ---------------------------------------------------------------------------

-- Build the current character's lockout snapshot, merging the API lockout state
-- with our own legacy kill log so other characters see legacy-raid kills too.
local function buildLockoutSnapshot()
    local out = {}
    if not (ETB.Lockouts and ETB.Lockouts.GetAll) then return out end
    local instances = ETB.Lockouts:GetAll()
    if type(instances) ~= "table" then return out end
    local logged = ETB.KillLog and ETB.KillLog.WasKilledThisLockout and ETB.KillLog.Dump and ETB.KillLog:Dump() or nil

    for _, rec in ipairs(instances) do
        local key = nkey(rec.name)
        if key then
            local kills = {}
            -- API per-boss kills (modern raids).
            if type(rec.encounters) == "table" then
                for bossKey, killed in pairs(rec.encounters) do
                    if killed then kills[bossKey] = true end
                end
            end
            -- Legacy kills from our own log.
            if ETB.KillLog and ETB.KillLog.WasKilledThisLockout then
                -- Fold in anything the kill log knows for this instance.
                if logged then
                    for _, e in ipairs(logged) do
                        if nkey(e.name) == key then
                            for _, bk in ipairs(e.bosses or {}) do kills[bk] = true end
                        end
                    end
                end
            end
            out[key] = {
                name = rec.name,
                resetAt = rec.resetAt,
                numEncounters = rec.numEncounters or 0,
                progress = rec.progress or 0,
                locked = rec.locked and true or false,
                isRaid = rec.isRaid and true or false,
                difficultyName = rec.difficultyName,
                kills = kills,
            }
        end
    end
    return out
end

local function buildWorldBossSnapshot()
    local out = {}
    if not (ETB.Lockouts and ETB.Lockouts.GetAll) then return out end
    local _, wbs = ETB.Lockouts:GetAll()
    if type(wbs) ~= "table" then return out end
    for _, wb in ipairs(wbs) do
        local key = nkey(wb.name)
        if key then out[key] = { name = wb.name, resetAt = wb.resetAt } end
    end
    return out
end

function AT:WriteSnapshot()
    local key = charKey()
    if not key then return end
    local _, classToken = UnitClass("player")
    local store = adb()
    store[key] = {
        name = UnitName("player"),
        realm = GetRealmName and GetRealmName() or "",
        class = classToken,
        armorType = self:ClassArmor(classToken),
        level = UnitLevel("player"),
        faction = UnitFactionGroup and UnitFactionGroup("player") or nil,
        lastSeen = now(),
        lockouts = buildLockoutSnapshot(),
        worldBosses = buildWorldBossSnapshot(),
    }
end

-- ---------------------------------------------------------------------------
-- Queries (consumed by Phase 2/3)
-- ---------------------------------------------------------------------------

-- All known characters as summaries (sorted by level desc, then name).
function AT:GetCharacters()
    local list = {}
    for key, c in pairs(adb()) do
        list[#list + 1] = {
            key = key, name = c.name, realm = c.realm, class = c.class,
            armorType = c.armorType, level = c.level or 0,
            faction = c.faction, lastSeen = c.lastSeen,
        }
    end
    table.sort(list, function(a, b)
        if (a.level or 0) ~= (b.level or 0) then return (a.level or 0) > (b.level or 0) end
        return (a.name or "") < (b.name or "")
    end)
    return list
end

-- Characters that can use a given armor type and meet a level floor.
function AT:GetEligibleChars(armorType, minLevel)
    local out = {}
    for _, c in ipairs(self:GetCharacters()) do
        local armorOK = (not armorType) or (c.armorType == armorType)
        local levelOK = (not minLevel) or ((c.level or 0) >= minLevel)
        if armorOK and levelOK then out[#out + 1] = c end
    end
    return out
end

-- This character's status for a specific boss, from its snapshot:
--   "killed"    — locked / boss down this reset (no more chances here)
--   "available" — not saved, or saved but this boss not yet down
--   "unknown"   — legacy raid with no per-boss data and no logged kill
function AT:CharBossStatus(key, instName, bossName)
    local c = adb()[key]
    if not c or not c.lockouts then return "available" end
    local rec = c.lockouts[nkey(instName)]
    if not rec then return "available" end                 -- not saved
    if rec.resetAt and now() > rec.resetAt then return "available" end  -- lockout expired

    local bk = nkey(bossName)
    if bk and rec.kills and rec.kills[bk] then return "killed" end
    if rec.numEncounters > 0 then
        if rec.progress >= rec.numEncounters then return "killed" end  -- fully cleared
        return "available"                                            -- partial, boss not down
    end
    if rec.locked then return "unknown" end                -- legacy, no per-boss data
    return "available"
end

-- Per-character weekly activity summary (cleared raids + world bosses this reset).
function AT:GetWeeklyActivity(key)
    local c = adb()[key]
    if not c then return nil end
    local raids, wbs = {}, {}
    if c.lockouts then
        for _, rec in pairs(c.lockouts) do
            if not (rec.resetAt and now() > rec.resetAt) then
                local done, total = 0, rec.numEncounters or 0
                if rec.kills then for _ in pairs(rec.kills) do done = done + 1 end end
                raids[#raids + 1] = {
                    name = rec.name, locked = rec.locked,
                    done = (total > 0 and rec.progress) or done, total = total,
                }
            end
        end
    end
    if c.worldBosses then
        for _, wb in pairs(c.worldBosses) do
            if not (wb.resetAt and now() > wb.resetAt) then wbs[#wbs + 1] = wb.name end
        end
    end
    return { raids = raids, worldBosses = wbs, lastSeen = c.lastSeen }
end

-- ---------------------------------------------------------------------------
-- Overview grid (Phase 3): instance-major matrix of every character's weekly
-- lockout state. Rows = instances + world bosses, columns = characters.
-- ---------------------------------------------------------------------------

-- One character's status for a whole instance (not a single boss):
--   "open"    — no lockout this reset; the character can run it
--   "partial" — saved with some bosses down (returns done,total too)
--   "done"    — fully cleared this reset
--   "locked"  — legacy raid locked with no per-boss data (can't tell progress)
function AT:InstanceStatus(charKey, instKey)
    local c = adb()[charKey]
    if not c or type(c.lockouts) ~= "table" then return "open" end
    local rec = c.lockouts[instKey]
    if not rec then return "open" end
    if rec.resetAt and now() > rec.resetAt then return "open" end
    local total = rec.numEncounters or 0
    if total > 0 then
        local prog = rec.progress or 0
        if prog >= total then return "done" end
        if prog > 0 then return "partial", prog, total end
        return "open"
    end
    if rec.locked then return "locked" end
    return "open"
end

-- Same as InstanceStatus but keyed by display name (for roster-driven rows such
-- as the heroic-dungeon grid, where we list every dungeon up front).
function AT:InstanceStatusByName(charKey, instName)
    return self:InstanceStatus(charKey, nkey(instName))
end

-- One character's status for a world boss: "done" if killed this reset, else "open".
function AT:WorldBossStatus(charKey, wbKey)
    local c = adb()[charKey]
    if not c or type(c.worldBosses) ~= "table" then return "open" end
    local wb = c.worldBosses[wbKey]
    if not wb then return "open" end
    if wb.resetAt and now() > wb.resetAt then return "open" end
    return "done"
end

-- Build the full overview: sorted characters, plus the union of every instance
-- and world boss any character has a live lockout for (so empty rows never show).
function AT:BuildOverview()
    local store = adb()
    local chars, instSet, wbSet = {}, {}, {}
    local n = now()
    for key, c in pairs(store) do
        chars[#chars + 1] = {
            key = key, name = c.name, class = c.class,
            level = c.level or 0, armorType = c.armorType,
        }
        if type(c.lockouts) == "table" then
            for ik, rec in pairs(c.lockouts) do
                if not (rec.resetAt and n > rec.resetAt) then
                    if not instSet[ik] then
                        instSet[ik] = { key = ik, name = rec.name, isRaid = rec.isRaid }
                    end
                end
            end
        end
        if type(c.worldBosses) == "table" then
            for wk, wb in pairs(c.worldBosses) do
                if not (wb.resetAt and n > wb.resetAt) then
                    wbSet[wk] = { key = wk, name = wb.name }
                end
            end
        end
    end
    table.sort(chars, function(a, b)
        if (a.level or 0) ~= (b.level or 0) then return (a.level or 0) > (b.level or 0) end
        return (a.name or "") < (b.name or "")
    end)
    -- Raids reset weekly; non-raid saved lockouts (heroic scenarios and the heroic
    -- dungeons you enter) reset daily, so isRaid cleanly separates the two sections.
    local instances, dailies = {}, {}
    for _, v in pairs(instSet) do
        if v.isRaid then
            instances[#instances + 1] = v
        else
            dailies[#dailies + 1] = v
        end
    end
    table.sort(instances, function(a, b) return (a.name or "") < (b.name or "") end)
    table.sort(dailies, function(a, b) return (a.name or "") < (b.name or "") end)
    -- Supplement with the curated current world-boss roster so bosses nobody has
    -- killed this week (and are therefore absent from every saved record) still show
    -- as open — exactly the case where seeing "who can still farm it" matters most.
    if ETB.WorldBossRoster then
        for i, nm in ipairs(ETB.WorldBossRoster) do
            local k = nkey(nm)
            if k then
                if wbSet[k] then
                    wbSet[k].order = i           -- killed record exists; keep tier order
                else
                    wbSet[k] = { key = k, name = nm, order = i }
                end
            end
        end
    end
    local worldBosses = {}
    for _, v in pairs(wbSet) do worldBosses[#worldBosses + 1] = v end
    table.sort(worldBosses, function(a, b)
        local ao, bo = a.order or 999, b.order or 999
        if ao ~= bo then return ao < bo end
        return (a.name or "") < (b.name or "")
    end)
    return { chars = chars, instances = instances, dailies = dailies, worldBosses = worldBosses }
end

-- Prune characters whose every lockout has expired AND haven't been seen in 30d?
-- No — we keep characters indefinitely (cheap), only their per-reset lockout data
-- naturally goes stale and is treated as "available" past resetAt.

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

function AT:Init()
    if self._inited then return end
    self._inited = true
    local EB = ETB.EventBus
    if not EB then return end
    -- Capture this character's identity immediately on every login/reload — this
    -- is the reliable trigger that guarantees the character enters the roster
    -- (eligibility only needs class/level/armor, which are always available).
    EB:Register("PLAYER_ENTERING_WORLD", function() AT:WriteSnapshot() end)
    -- Also snapshot once logged in, and refresh whenever lockouts change so the
    -- per-boss data stays current for the logged-in character.
    EB:Register("PLAYER_LOGIN", function() AT:WriteSnapshot() end)
    EB:Register("ETB_LOCKOUTS_UPDATED", function() AT:WriteSnapshot() end)
end

AT:Init()

ETB:Debug("AltTracker", "Account character tracker loaded")

return AT
