-- TOG Tools — Login Digest (engine)
-- Prints a single-line state-of-account summary at PLAYER_LOGIN.
--
-- Fields (each toggleable in the tab; suppressed when 0/empty/N/A):
--   mail        — mail expiring soon across all alts (reads addon.modules.mailbox:GetExpiringSoon)
--   guildOnline — guild members currently online
--   hearth      — current char's hearthstone bind location
--   bags        — current char's bag-slot occupancy percentage
--   lastLogout  — how long since this character last logged out (per-char data)
--
-- Last logout is stamped on PLAYER_LOGOUT for the current character into
-- db.global.loginDigest.lastLogout[Name-Realm], read at login time.

local _, addon = ...

local LoginDigest = {}
addon.modules["logindigest"] = LoginDigest

-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
local function CharKey()
    local realm = (GetRealmName() or ""):gsub("%s+", "")
    return (UnitName("player") or "?") .. "-" .. realm
end

local function DigestDB()
    return addon.DB and addon.DB.global and addon.DB.global.loginDigest
end

-- Audit round 4 finding G. `if not (db and db.enabled)` defaulted a MISSING flag
-- to OFF; addon:IsModuleEnabled defaults it to ON by explicit design. One
-- predicate, one place where the default is decided. See Mailbox.lua for the
-- same note; Gratz:124 and Smack:156 were already doing it this way.
local function isEnabled()
    return addon:IsModuleEnabled("logindigest")
end

-- C_Container is the Retail 10+ namespace for bag APIs. Classic still uses
-- the unprefixed globals. Either path returns the same numbers.
local function BagSlots(bag)
    if C_Container and C_Container.GetContainerNumSlots then
        return C_Container.GetContainerNumSlots(bag) or 0
    end
    return (GetContainerNumSlots and GetContainerNumSlots(bag)) or 0
end

local function BagFree(bag)
    if C_Container and C_Container.GetContainerNumFreeSlots then
        return C_Container.GetContainerNumFreeSlots(bag) or 0
    end
    return (GetContainerNumFreeSlots and GetContainerNumFreeSlots(bag)) or 0
end

local function FormatDuration(seconds)
    if not seconds or seconds < 0 then return nil end
    if seconds < 3600  then return string.format("%dm", math.floor(seconds / 60))   end
    if seconds < 86400 then return string.format("%dh", math.floor(seconds / 3600)) end
    return string.format("%dd", math.floor(seconds / 86400))
end

-- ---------------------------------------------------------------------------
-- Field builders — each returns (text, true) on success, or (nil, false) to
-- suppress the field for this print.
-- ---------------------------------------------------------------------------
local fieldBuilders = {}

function fieldBuilders.mail()
    local Mailbox = addon.modules and addon.modules["mailbox"]
    if not (Mailbox and Mailbox.GetExpiringSoon) then return nil, false end
    local total, _, threshold = Mailbox:GetExpiringSoon()
    if total <= 0 then return nil, false end
    return string.format("|cffffaa55Mail:|r %d in <=%dd", total, threshold), true
end

function fieldBuilders.guildOnline()
    if not IsInGuild or not IsInGuild() then return nil, false end
    if not GetNumGuildMembers then return nil, false end
    -- GetNumGuildMembers returns (total, online) on EVERY flavour -- measured on
    -- live Classic Era and Retail clients, not inferred (WoWAPITesting
    -- env/guild.lua:188-199). This used to read only `total` and then count
    -- online itself by walking GetGuildRosterInfo 1..total, which is the same
    -- answer at the cost of one call per guild member AT LOGIN, the moment the
    -- client is most loaded -- 900 calls in a large guild, for a number the API
    -- already returned. The loop survives only as a fallback for a client that
    -- does not supply the second value (audit round 13, finding tog-AB).
    local total, online = GetNumGuildMembers()
    total = total or 0
    if total <= 0 then return nil, false end
    if not online then
        online = 0
        for i = 1, total do
            local _, _, _, _, _, _, _, _, isOnline = GetGuildRosterInfo(i)
            if isOnline then online = online + 1 end
        end
    end
    if online <= 0 then return nil, false end
    return string.format("|cff44ee44Guild:|r %d online", online), true
end

function fieldBuilders.hearth()
    local loc = GetBindLocation and GetBindLocation()
    if not loc or loc == "" then return nil, false end
    return string.format("|cffaaaaffHearth:|r %s", loc), true
end

-- Highest carried-bag container index, INCLUDING Retail's reagent bag.
--
-- This was hardcoded to 4 -- backpack (0) plus four bag slots -- which is right
-- on every Classic flavour and WRONG on Retail, where 10.0 added a fifth
-- equipped bag at index 5 and its slots were simply left out of the percentage
-- (audit round 13, finding tog-AA). Verified per flavour in the client source
-- rather than assumed: Retail's Blizzard_FrameXMLBase/Constants.lua:184-186
-- defines NUM_TOTAL_EQUIPPED_BAG_SLOTS = NUM_BAG_SLOTS + NUM_REAGENTBAG_SLOTS,
-- and the Classic Era tree's Classic/Constants.lua:209 defines NUM_BAG_SLOTS
-- alone, with no reagent constant at all. So the first global is the right
-- answer where it exists and is absent exactly where it would be wrong; the
-- literal 4 is the last-resort fallback and is the Classic value.
--
-- Bank bags stay excluded either way -- they are not the "do I need to clean up"
-- signal this field is for.
local function LastBagIndex()
    return NUM_TOTAL_EQUIPPED_BAG_SLOTS or NUM_BAG_SLOTS or 4
end

function fieldBuilders.bags()
    local total, free = 0, 0
    for bag = 0, LastBagIndex() do
        total = total + BagSlots(bag)
        free  = free  + BagFree(bag)
    end
    if total <= 0 then return nil, false end
    local pct = math.floor(((total - free) / total) * 100 + 0.5)
    return string.format("|cffffcc66Bags:|r %d%% full", pct), true
end

function fieldBuilders.lastLogout()
    local db = DigestDB()
    if not (db and db.lastLogout) then return nil, false end
    local last = db.lastLogout[CharKey()]
    if not last then return nil, false end
    local delta = GetServerTime() - last
    local txt   = FormatDuration(delta)
    if not txt then return nil, false end
    return string.format("|cff999999Last logout:|r %s ago", txt), true
end

-- Field render order. Mail first because it's actionable (you can lose
-- mail to expiration). Last logout last because it's curiosity, not action.
local FIELD_ORDER = { "mail", "guildOnline", "hearth", "bags", "lastLogout" }

-- ---------------------------------------------------------------------------
-- Print
-- ---------------------------------------------------------------------------
function LoginDigest:Print()
    local db = DigestDB()
    if not db or not isEnabled() then return end

    local fields = db.fields or {}
    local parts = {}
    for _, key in ipairs(FIELD_ORDER) do
        if fields[key] ~= false then
            local builder = fieldBuilders[key]
            if builder then
                local ok, text, show = pcall(builder)
                if ok and show and text then
                    parts[#parts + 1] = text
                end
            end
        end
    end

    if #parts == 0 then return end

    print(string.format(
        "|c%s[TOGTools]|r %s",
        addon.BrandColor or "ffFF8000",
        table.concat(parts, "  |cff666666|||r  ")
    ))
end

-- ---------------------------------------------------------------------------
-- Logout stamp
-- ---------------------------------------------------------------------------
-- DELIBERATELY UNGATED, and a round-4 audit finding (F) says it should be gated. Disagreeing, with
-- the reason, because the consequence runs the other way: the stamp is the "since" bound of the next
-- digest. Skip it while the module is off and the first digest after re-enabling spans the ENTIRE
-- disabled period -- disable for a week, re-enable, and you are shown a week of mail and gold as
-- though it happened overnight. Stamping while disabled costs one number per logout and keeps that
-- first digest correct. The finding is right that this is an ungated write; it is the one place
-- where the write is what makes the gate's own feature work.
local function StampLogout()
    local db = DigestDB()
    if not db then return end
    db.lastLogout = db.lastLogout or {}
    db.lastLogout[CharKey()] = GetServerTime()
end

-- ---------------------------------------------------------------------------
-- Event wiring
-- ---------------------------------------------------------------------------
local _frame = CreateFrame("Frame")
_frame:RegisterEvent("PLAYER_LOGIN")
_frame:RegisterEvent("PLAYER_LOGOUT")
_frame:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_LOGIN" then
        -- Kick the guild roster refresh; results land asynchronously via
        -- GUILD_ROSTER_UPDATE. The 3s deferral below gives roster + mail
        -- + bag data time to settle before we read them.
        -- Gated: a disabled module has no business sending a server request. The roster is only
        -- ever read by Print, which returns immediately when disabled, so this was a login-time
        -- request on behalf of output nobody was going to see. (Round-4 audit finding F.)
        if isEnabled() then
            if GuildRoster then pcall(GuildRoster) end
        end
        C_Timer.After(3, function() LoginDigest:Print() end)
    elseif event == "PLAYER_LOGOUT" then
        StampLogout()
    end
end)

function LoginDigest:OnEnable()
    -- Event wiring above runs at file load. Hook is here for module-pattern
    -- consistency in case OnEnable is needed later.
end
