-- TOG Tools — Whisper Log (engine)
--
-- Captures every whisper sent and received (both in-game whispers and
-- Battle.net whispers) into a flat list under `db.global.whisperLog.entries`.
--
-- Entry shape:
--   { ts, dir = "in"|"out", player, other, text,
--     isGM, isBN, guid, bnSenderID, lineID, zone }
--
-- Hook recipe (plain event subscriptions — no global API replacement):
--   * CHAT_MSG_WHISPER             — incoming in-game whisper
--   * CHAT_MSG_WHISPER_INFORM      — outgoing in-game whisper (we sent it)
--   * CHAT_MSG_BN_WHISPER          — incoming Battle.net whisper
--   * CHAT_MSG_BN_WHISPER_INFORM   — outgoing Battle.net whisper
--
-- Payload (canonical 17-arg shape, identical across Classic + Retail per
-- the Blizzard API docs mirror at
-- `Blizzard_APIDocumentationGenerated/ChatInfoDocumentation.lua`):
--   arg1  = text
--   arg2  = playerName (sender for *_WHISPER, recipient for *_INFORM)
--   arg6  = specialFlags ("GM" for Blizzard GM whispers — same test used
--           by Blizzard_GMChatUI.lua)
--   arg11 = lineID (used here for cross-filter-chain dedup)
--   arg12 = guid (in-game) or nil (BN events don't carry a player GUID)
--   arg13 = bnSenderID (BN events) or nil (in-game)
--
-- Sub-category registration mirrors MailLog / TradeLog: NOT in
-- addon.modules; only in addon.logCategories["whispers"]. Sub-tab Draw
-- is merged on by GUI/WhisperLogSubTab.lua.

local _, addon = ...

local WhisperLog = {}
addon.logCategories = addon.logCategories or {}
addon.logCategories["whispers"] = WhisperLog

WhisperLog.subKey    = "whispers"
WhisperLog.label     = "Whispers"
WhisperLog.configKey = "whisperLog"
WhisperLog.help = {
    title = "Logs > Whispers",
    lines = {
        "Every whisper sent or received is appended here automatically — both in-game whispers and Battle.net whispers.",
        "|cffffd700Other column:|r |cff66ff66<-|r prefix = received from this player; |cffffaa55->|r prefix = sent to this player. Optional |cffaa44ff[BN]|r tag = Battle.net whisper, |cffff5555[GM]|r tag = Blizzard GM whisper.",
        "|cffffd700Right-click a partner name|r (in-game whispers) to pop the standard chat-name menu — whisper, invite, inspect, ignore, report — same one you get clicking a name in chat. Left-click opens a whisper edit box directly.",
        "|cffffd700Filters:|r Character (per-alt), Direction (Received / Sent / Both), Type (In-game / Battle.net / Both), GM (All / Hide GM / GM only), Date range, plus a |cffffd700Partner|r and |cffffd700Text|r substring search row underneath. Press Enter in either text box to apply.",
        "|cffffd700De-duplication:|r WoW occasionally re-fires chat events through filter chains. A 5-second recent-lineID cache drops repeats so each whisper is logged exactly once.",
        "|cffffd700Clear data:|r centralised in |cffffd700ESC > Options > AddOns > TOG Tools > Clear Data > Clear Whisper Log|r.",
    },
}

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

local function WhisperLogDB()
    return addon.DB and addon.DB.global and addon.DB.global.whisperLog
end

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

-- Refresh the Whispers sub-tab when it's the active sub-tab inside an open
-- Logs tab. Cheap and idempotent — re-reads entries from SV.
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
    if frames and frames.logsActiveSubTab == "whispers" and mw.Refresh then
        mw:Refresh()
    end
end

-- ---------------------------------------------------------------------------
-- Recent-lineID cache (dedup)
-- ---------------------------------------------------------------------------
-- WoW chat events can re-fire through multiple filter chains (other addons
-- hooking ChatFrame_AddMessageEventFilter, the chat frame re-emitting on
-- tab moves, etc.). lineID is a server-assigned unique number per chat
-- message, so a 5-second recent-set keyed by lineID drops repeats without
-- losing real back-to-back whispers (which have distinct lineIDs).
local DEDUP_WINDOW = 5
local _recentLines = {}

local function seenRecently(lineID)
    if not lineID or lineID == 0 then return false end
    local now = (GetServerTime and GetServerTime()) or time()
    for k, t in pairs(_recentLines) do
        if (now - t) > DEDUP_WINDOW then _recentLines[k] = nil end
    end
    if _recentLines[lineID] then return true end
    _recentLines[lineID] = now
    return false
end

-- ---------------------------------------------------------------------------
-- Capture
-- ---------------------------------------------------------------------------
local function append(dir, isBN, text, playerName, specialFlags, lineID, guid, bnSenderID)
    if not isEnabled() then return end
    if not text or text == "" then return end
    if seenRecently(lineID) then return end
    local db = WhisperLogDB()
    if not db then return end
    db.entries = db.entries or {}

    tinsert(db.entries, {
        ts         = GetServerTime() or time(),
        dir        = dir,
        player     = CharKey(),
        other      = playerName or "?",
        text       = text,
        isGM       = specialFlags == "GM",
        isBN       = isBN and true or false,
        guid       = guid,
        bnSenderID = bnSenderID,
        lineID     = lineID,
        zone       = (GetRealZoneText and GetRealZoneText()) or nil,
    })

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

local _evtFrame = CreateFrame("Frame")
_evtFrame:RegisterEvent("CHAT_MSG_WHISPER")
_evtFrame:RegisterEvent("CHAT_MSG_WHISPER_INFORM")
_evtFrame:RegisterEvent("CHAT_MSG_BN_WHISPER")
_evtFrame:RegisterEvent("CHAT_MSG_BN_WHISPER_INFORM")
_evtFrame:SetScript("OnEvent", function(_, event, ...)
    -- Payload positions confirmed against Blizzard's docs mirror; same
    -- layout for all four whisper events and stable across versions.
    local text         = select(1,  ...)
    local playerName   = select(2,  ...)
    local specialFlags = select(6,  ...)
    local lineID       = select(11, ...)
    local guid         = select(12, ...)
    local bnSenderID   = select(13, ...)

    if event == "CHAT_MSG_WHISPER" then
        append("in",  false, text, playerName, specialFlags, lineID, guid, nil)
    elseif event == "CHAT_MSG_WHISPER_INFORM" then
        append("out", false, text, playerName, specialFlags, lineID, guid, nil)
    elseif event == "CHAT_MSG_BN_WHISPER" then
        append("in",  true,  text, playerName, specialFlags, lineID, nil,  bnSenderID)
    elseif event == "CHAT_MSG_BN_WHISPER_INFORM" then
        append("out", true,  text, playerName, specialFlags, lineID, nil,  bnSenderID)
    end
end)

-- ---------------------------------------------------------------------------
-- Read API consumed by GUI/WhisperLogSubTab.lua
-- ---------------------------------------------------------------------------
--- Returns a newest-first list of entries matching the filters.
---   filterPlayer  string|nil    character key (Name-Realm), nil = all alts
---   filterDir     string|nil    "in" / "out", nil = both
---   filterType    string|nil    "ingame" / "bn", nil = both
---   filterGM      string|nil    "gm" (only) / "nogm" (hide), nil = all
---   filterPartner string|nil    case-insensitive substring on `other`
---   filterText    string|nil    case-insensitive substring on `text`
---   fromTs, toTs  number|nil    inclusive bounds (nil = unbounded)
function WhisperLog:GetEntries(filterPlayer, filterDir, filterType, filterGM,
                               filterPartner, filterText, fromTs, toTs)
    local db = WhisperLogDB()
    if not db or not db.entries then return {} end
    local partnerLower = (filterPartner and filterPartner ~= "") and filterPartner:lower() or nil
    local textLower    = (filterText    and filterText    ~= "") and filterText:lower()    or nil
    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 filterType then
            if filterType == "ingame" and e.isBN then ok = false
            elseif filterType == "bn" and not e.isBN then ok = false end
        end
        if ok and filterGM then
            if filterGM == "gm" and not e.isGM then ok = false
            elseif filterGM == "nogm" and e.isGM then ok = false end
        end
        if ok and partnerLower then
            local other = (e.other or ""):lower()
            if not other:find(partnerLower, 1, true) then ok = false end
        end
        if ok and textLower then
            local body = (e.text or ""):lower()
            if not body:find(textLower, 1, true) then ok = false end
        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.
function WhisperLog:GetPlayerKeys()
    local db = WhisperLogDB()
    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 WhisperLog:ClearAll()
    local db = WhisperLogDB()
    if db then db.entries = {} end
    refreshSubTab()
end
