-- TOG Tools — Mail Log (engine)
--
-- Captures every mail received (taken from the inbox) and sent (composed
-- via SendMail). Append-only flat list under `db.global.mailLog.entries`.
--
-- Entry shape:
--   { ts, dir = "in"|"out", player, other, subject,
--     items = { { link, count }, ... }, money, cod }
--
-- Receive hook recipe (standard WoW mail-API pattern):
--   * hooksecurefunc("TakeInboxItem", MailIndex, ItemIndex)
--   * hooksecurefunc("AutoLootMailItem", MailIndex)       -- "Take All"
--   * hooksecurefunc("TakeInboxMoney", MailIndex)
-- Each take fires while the mail row is being processed; consecutive
-- takes from the same sender within MERGE_WINDOW_SEC merge into the
-- most-recent entry so a 12-item "Take All" produces one row, not 12.
--
-- Send hook recipe:
--   * hooksecurefunc("SendMail", recipient, subject, body)
--     captures recipient + subject before the actual send.
--   * MAIL_SEND_INFO_UPDATE event rebuilds the staged item cache (the
--     compose window's 12 attachment slots).
--   * UI_INFO_MESSAGE with arg2 == ERR_MAIL_SENT commits the entry.
--
-- Sub-category registration: MailLog is NOT a top-level addon.modules
-- entry. It lives in addon.logCategories["mail"], where the Logs outer
-- tab's nested TabGroup finds it. The sub-tab Draw is merged on by
-- GUI/MailLogSubTab.lua.

local _, addon = ...

local MailLog = {}
addon.logCategories = addon.logCategories or {}
addon.logCategories["mail"] = MailLog

MailLog.subKey    = "mail"
MailLog.label     = "Mail"
MailLog.configKey = "mailLog"
MailLog.help = {
    title = "Logs > Mail",
    lines = {
        "Every mail received (inbox takes) and sent (composed via the in-game mail UI) is appended here automatically.",
        "|cffffd700Receive merging:|r consecutive takes from the same sender within 5 seconds collapse into one entry, so a 12-item Take-All produces one row, not 12. Items in that entry are stacked by link.",
        "|cffffd700Filters:|r Character (per-alt), Direction (Received / Sent / Both), Date range. Newest entries at the top by default — click any column header to re-sort.",
        "|cffffd700Other column:|r |cff66ff66<-|r prefix = received from this sender; |cffffaa55->|r prefix = sent to this recipient.",
        "|cffffd700Items column:|r leads with the first item link; multi-item mails show \"+N more\" on the right.",
        "|cffffd700Clear data:|r two-step confirm. First click flips to \"Really? (5s)\"; second click within 5 s wipes every entry. Lets the timer expire to cancel.",
    },
}

local MERGE_WINDOW_SEC = 5

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

local function MailLogDB()
    return addon.DB and addon.DB.global and addon.DB.global.mailLog
end

local function isEnabled()
    return addon:IsLogCategoryEnabled("mail")
end

-- Refresh the Mail sub-tab when it's the active sub-tab inside an open
-- Logs tab. The outer MainWindow:Refresh re-runs DrawTab for the active
-- outer tab; LogsTab redraws its inner TabGroup and calls MailLog:Draw
-- which re-reads entries from SV. Cheap and idempotent.
local function refreshSubTab()
    local mw = addon.MainWindow
    if not (mw and mw.activeTab == "logs") then return end
    local frames = addon.DB and addon.DB.char and addon.DB.char.frames
    local activeSub = frames and frames.logsActiveSubTab
    if activeSub == "mail" and mw.Refresh then
        mw:Refresh()
    end
end

-- ---------------------------------------------------------------------------
-- Receive capture
-- ---------------------------------------------------------------------------
-- Per-MailIndex cache of "currently-being-taken" entries. Items, money,
-- and COD-from-header updates for the same mail merge into one entry;
-- a take on a different MailIndex creates a new entry even if the
-- sender matches. The earlier "merge by last-entry sender + 5 sec
-- window" lost per-mail granularity when an addon (Postal, etc.) opened
-- multiple mails from the same sender in quick succession — they all
-- collapsed into one row. Cache is wiped on MAIL_CLOSED.
local _receiveCache = {}  -- [MailIndex] = entry

local function appendReceive(MailIndex, itemLink, itemCount, money)
    if not isEnabled() then return end
    local db = MailLogDB()
    if not db then return end
    db.entries = db.entries or {}

    local _, _, sender, subject, _, codAmount = GetInboxHeaderInfo(MailIndex)
    sender  = sender  or "?"
    subject = subject or ""
    local now    = GetServerTime() or time()
    local player = CharKey()

    -- Reuse the cached entry for this MailIndex if it's still recent and
    -- the sender still matches (defensive — if the mailbox got refreshed
    -- and indices shifted, the cached entry is for a stale mail at the
    -- same slot and we should NOT merge into it). Otherwise create a
    -- fresh entry per mail.
    local cached = _receiveCache[MailIndex]
    local mergeable = cached
        and cached.other == sender
        and (now - (cached.ts or 0)) <= MERGE_WINDOW_SEC
    local entry
    if mergeable then
        entry    = cached
        entry.ts = now
    else
        entry = {
            ts      = now,
            dir     = "in",
            player  = player,
            other   = sender,
            subject = subject,
            items   = {},
            money   = 0,
            cod     = (codAmount or 0) > 0,
        }
        tinsert(db.entries, entry)
        _receiveCache[MailIndex] = entry
    end

    if itemLink and (itemCount or 0) > 0 then
        -- Merge by itemLink so a 5×5 stack from "Take All" shows as one
        -- row "[Link] ×5" rather than five rows of ×1.
        local merged = false
        for _, it in ipairs(entry.items) do
            if it.link == itemLink then
                it.count = (it.count or 0) + itemCount
                merged = true
                break
            end
        end
        if not merged then
            tinsert(entry.items, { link = itemLink, count = itemCount })
        end
    end
    if money and money > 0 then
        entry.money = (entry.money or 0) + money
    end

    local Logs = addon.modules and addon.modules["logs"]
    if Logs and Logs.PruneEntries then Logs:PruneEntries(db.entries) end
    refreshSubTab()
end

hooksecurefunc("TakeInboxItem", function(MailIndex, ItemIndex)
    if not isEnabled() then return end
    local itemLink = GetInboxItemLink(MailIndex, ItemIndex)
    local _, _, _, count = GetInboxItem(MailIndex, ItemIndex)
    appendReceive(MailIndex, itemLink, count or 1, 0)
end)

hooksecurefunc("AutoLootMailItem", function(MailIndex)
    if not isEnabled() then return end
    -- Walk every attachment slot (12 max). GetInboxItemLink returns nil
    -- for empty slots so we filter on link presence.
    for i = 1, 12 do
        local itemLink = GetInboxItemLink(MailIndex, i)
        if itemLink then
            local _, _, _, count = GetInboxItem(MailIndex, i)
            appendReceive(MailIndex, itemLink, count or 1, 0)
        end
    end
end)

hooksecurefunc("TakeInboxMoney", function(MailIndex)
    if not isEnabled() then return end
    local _, _, _, _, money = GetInboxHeaderInfo(MailIndex)
    appendReceive(MailIndex, nil, 0, money or 0)
end)

-- ---------------------------------------------------------------------------
-- Send capture
-- ---------------------------------------------------------------------------
local _pendingSend = nil   -- { recipient, subject, items, money }

local function rebuildSendItemsCache()
    _pendingSend = _pendingSend or {}
    _pendingSend.items = {}
    for i = 1, 12 do
        local link = GetSendMailItemLink(i)
        if link then
            local _, _, _, count = GetSendMailItem(i)
            local merged = false
            for _, it in ipairs(_pendingSend.items) do
                if it.link == link then
                    it.count = (it.count or 0) + (count or 1)
                    merged = true
                    break
                end
            end
            if not merged then
                tinsert(_pendingSend.items, { link = link, count = count or 1 })
            end
        end
    end
end

hooksecurefunc("SendMail", function(recipient, subject)
    if not isEnabled() then return end
    _pendingSend = _pendingSend or {}
    _pendingSend.recipient = recipient
    _pendingSend.subject   = subject
end)

local _evtFrame = CreateFrame("Frame")
_evtFrame:RegisterEvent("MAIL_SEND_INFO_UPDATE")
_evtFrame:RegisterEvent("MAIL_CLOSED")
_evtFrame:RegisterEvent("UI_INFO_MESSAGE")
_evtFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "MAIL_SEND_INFO_UPDATE" then
        if isEnabled() then rebuildSendItemsCache() end

    elseif event == "MAIL_CLOSED" then
        -- Compose window is gone; drop any in-flight send cache. Also
        -- wipe the per-MailIndex receive cache so re-opening the mail-
        -- box starts fresh — indices may have shifted between visits.
        _pendingSend = nil
        wipe(_receiveCache)

    elseif event == "UI_INFO_MESSAGE" then
        local _, msg = ...
        if msg == ERR_MAIL_SENT and isEnabled()
           and _pendingSend and _pendingSend.recipient then
            local db = MailLogDB()
            if not db then return end
            db.entries = db.entries or {}
            -- Re-read items + money at commit time so anything added
            -- between MAIL_SEND_INFO_UPDATE and the send confirmation
            -- is captured.
            rebuildSendItemsCache()
            local money = GetSendMailMoney() or 0
            tinsert(db.entries, {
                ts      = GetServerTime() or time(),
                dir     = "out",
                player  = CharKey(),
                other   = _pendingSend.recipient,
                subject = _pendingSend.subject or "",
                items   = _pendingSend.items or {},
                money   = money,
                cod     = false,
            })
            local Logs = addon.modules and addon.modules["logs"]
            if Logs and Logs.PruneEntries then Logs:PruneEntries(db.entries) end
            _pendingSend = nil
            refreshSubTab()
        end
    end
end)

-- ---------------------------------------------------------------------------
-- Read API consumed by GUI/MailLogSubTab.lua
-- ---------------------------------------------------------------------------
--- Returns a newest-first list of entries matching the filters.
---   filterPlayer  string|nil    character key, or nil/empty = no player filter
---   filterDir     string|nil    "in" / "out", or nil = both
---   fromTs, toTs  number|nil    inclusive bounds (nil = unbounded)
function MailLog:GetEntries(filterPlayer, filterDir, fromTs, toTs)
    local db = MailLogDB()
    if not db or not db.entries then return {} end
    local out = {}
    for _, e in ipairs(db.entries) do
        local ok = true
        if filterPlayer and filterPlayer ~= "" and e.player ~= filterPlayer then ok = false end
        if ok and filterDir and e.dir ~= filterDir then ok = false end
        if ok and fromTs and (e.ts or 0) < fromTs then ok = false end
        if ok and toTs   and (e.ts or 0) > toTs   then ok = false end
        if ok then tinsert(out, e) end
    end
    table.sort(out, function(a, b) return (a.ts or 0) > (b.ts or 0) end)
    return out
end

--- Unique character keys seen in the log; alphabetically sorted. Used by
--- the sub-tab's character-filter dropdown.
function MailLog:GetPlayerKeys()
    local db = MailLogDB()
    if not db or not db.entries then return {} end
    local seen = {}
    for _, e in ipairs(db.entries) do
        if e.player then seen[e.player] = true end
    end
    local list = {}
    for k in pairs(seen) do tinsert(list, k) end
    table.sort(list)
    return list
end

function MailLog:ClearAll()
    local db = MailLogDB()
    if db then db.entries = {} end
    refreshSubTab()
end
