-- TOG Tools — Trade Log (engine)
--
-- Captures every completed player-to-player trade into a flat list under
-- `db.global.tradeLog.entries`. Two-sided entry: separate `given` /
-- `received` item lists, separate money totals, and a dedicated
-- `enchantGiven` / `enchantReceived` pair for trade slot 7 (the enchant
-- slot — applying an enchant goes there, not in the regular item list).
--
-- Entry shape:
--   { ts, player, other, given = {{link,count},...}, received = {{...}},
--     moneyGiven, moneyReceived, enchantGiven, enchantReceived, zone }
--
-- Hook recipe (standard WoW trade-API pattern):
--   * TRADE_SHOW                       open the staging cache
--   * TRADE_PLAYER_ITEM_CHANGED(slot)  player side, slot 1-6 = items, 7 = enchant
--   * TRADE_TARGET_ITEM_CHANGED(slot)  target side, same slot semantics
--   * TRADE_MONEY_CHANGED              both money fields refresh
--   * TRADE_ACCEPT_UPDATE              re-read everything (covers any
--                                      slot we missed an event for)
--   * UI_INFO_MESSAGE → ERR_TRADE_COMPLETE  commit to entries
--   * MAIL_CLOSED / TRADE_CLOSED       discard staging if uncompleted
--
-- Sub-category registration mirrors MailLog: NOT in addon.modules; only
-- in addon.logCategories["trade"]. Sub-tab Draw is merged on by
-- GUI/TradeLogSubTab.lua.

local _, addon = ...

local TradeLog = {}
addon.logCategories = addon.logCategories or {}
addon.logCategories["trade"] = TradeLog

TradeLog.subKey    = "trade"
TradeLog.label     = "Trade"
TradeLog.configKey = "tradeLog"
TradeLog.help = {
    title = "Logs > Trade",
    lines = {
        "Every completed player-to-player trade is appended here automatically. Arena/BG trades are skipped on purpose — they're noisy and rarely useful.",
        "|cffffd700Two-sided entries:|r |cffffd700Given|r is what you handed over (items + money); |cffffd700Received|r is what you got back.",
        "|cffffd700Enchant column:|r trade slot 7 is the enchant slot. |cffaa44ff->|r = enchant applied to partner's item; |cffaa44ff<-|r = partner applied an enchant to your item; both arrows = mutual enchant trade.",
        "|cffffd700Filters:|r Character (per-alt), Date range. Click any column header to re-sort; newest first by default.",
        "|cffffd700Inspection-only opens|r (both sides empty, no money, no enchant) are discarded — only completed trades that actually moved something are logged.",
        "|cffffd700Clear data:|r two-step confirm — see Mail tab for details.",
    },
}

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

local function TradeLogDB()
    return addon.DB and addon.DB.global and addon.DB.global.tradeLog
end

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

local function inPvPInstance()
    -- Arena/BG returns "pvp" or "arena" as the second IsInInstance value.
    -- Logging trades there is noisy and rarely useful — skip.
    if not IsInInstance then return false end
    local _, instType = IsInInstance()
    return instType == "pvp" or instType == "arena"
end

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 == "trade" and mw.Refresh then
        mw:Refresh()
    end
end

-- ---------------------------------------------------------------------------
-- Per-trade staging cache. Built on TRADE_SHOW, mutated by slot/money
-- changes, committed (to db.entries) on ERR_TRADE_COMPLETE, discarded on
-- TRADE_CLOSED. Two slot-keyed maps (1-6 items + 7 enchant) keep the
-- last-known state of each slot regardless of update order.
-- ---------------------------------------------------------------------------
local _staging = nil

local function newStaging()
    return {
        playerItems = {},     -- [slot] = { link, count }
        targetItems = {},     -- [slot] = { link, count }
        playerMoney = 0,
        targetMoney = 0,
    }
end

local function readPlayerSlot(slot)
    if not GetTradePlayerItemLink or not GetTradePlayerItemInfo then return nil end
    local link = GetTradePlayerItemLink(slot)
    if not link then return nil end
    -- Classic / Retail signature shifts: name, texture, count, quality, enchant?
    -- The count field is consistently the 3rd return.
    local _, _, count = GetTradePlayerItemInfo(slot)
    return { link = link, count = count or 1 }
end

local function readTargetSlot(slot)
    if not GetTradeTargetItemLink or not GetTradeTargetItemInfo then return nil end
    local link = GetTradeTargetItemLink(slot)
    if not link then return nil end
    local _, _, count = GetTradeTargetItemInfo(slot)
    return { link = link, count = count or 1 }
end

local function snapshotAllSlots()
    if not _staging then return end
    for slot = 1, 7 do
        _staging.playerItems[slot] = readPlayerSlot(slot)
        _staging.targetItems[slot] = readTargetSlot(slot)
    end
    if GetPlayerTradeMoney then _staging.playerMoney = GetPlayerTradeMoney() or 0 end
    if GetTargetTradeMoney then _staging.targetMoney = GetTargetTradeMoney() or 0 end
end

-- Reduce the slot-keyed staging map to an ordered list with same-link
-- counts merged (mirrors MailLog's same-link merge for compact rendering).
local function slotsToList(slotMap)
    if not slotMap then return {} end
    local seen = {}
    local list = {}
    for slot = 1, 6 do   -- slots 1-6 only; 7 is enchant and handled separately
        local entry = slotMap[slot]
        if entry and entry.link then
            local existing = seen[entry.link]
            if existing then
                existing.count = (existing.count or 0) + (entry.count or 1)
            else
                local copy = { link = entry.link, count = entry.count or 1 }
                seen[entry.link] = copy
                tinsert(list, copy)
            end
        end
    end
    return list
end

-- ---------------------------------------------------------------------------
-- Event wiring
-- ---------------------------------------------------------------------------
local _evtFrame = CreateFrame("Frame")
_evtFrame:RegisterEvent("TRADE_SHOW")
_evtFrame:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED")
_evtFrame:RegisterEvent("TRADE_TARGET_ITEM_CHANGED")
_evtFrame:RegisterEvent("TRADE_MONEY_CHANGED")
_evtFrame:RegisterEvent("TRADE_ACCEPT_UPDATE")
_evtFrame:RegisterEvent("TRADE_CLOSED")
_evtFrame:RegisterEvent("UI_INFO_MESSAGE")
_evtFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "TRADE_SHOW" then
        if not isEnabled() or inPvPInstance() then return end
        _staging = newStaging()
        snapshotAllSlots()

    elseif event == "TRADE_PLAYER_ITEM_CHANGED" then
        if not _staging then return end
        local slot = ...
        if slot then _staging.playerItems[slot] = readPlayerSlot(slot) end

    elseif event == "TRADE_TARGET_ITEM_CHANGED" then
        if not _staging then return end
        local slot = ...
        if slot then _staging.targetItems[slot] = readTargetSlot(slot) end

    elseif event == "TRADE_MONEY_CHANGED" then
        if not _staging then return end
        if GetPlayerTradeMoney then _staging.playerMoney = GetPlayerTradeMoney() or 0 end
        if GetTargetTradeMoney then _staging.targetMoney = GetTargetTradeMoney() or 0 end

    elseif event == "TRADE_ACCEPT_UPDATE" then
        -- Both sides accepting — re-snapshot everything so we don't miss
        -- any slot whose change event we never received (e.g. drag-and-
        -- drop sequences that fire multiple state changes per frame).
        snapshotAllSlots()

    elseif event == "TRADE_CLOSED" then
        -- The trade window closed. If it closed because of ERR_TRADE_COMPLETE
        -- we've already committed; otherwise it was cancelled or errored
        -- and we just drop the staging.
        _staging = nil

    elseif event == "UI_INFO_MESSAGE" then
        local _, msg = ...
        if msg == ERR_TRADE_COMPLETE and _staging and isEnabled() then
            local db = TradeLogDB()
            if not db then return end
            db.entries = db.entries or {}

            local given    = slotsToList(_staging.playerItems)
            local received = slotsToList(_staging.targetItems)
            local enchantGiven    = _staging.playerItems[7] and _staging.playerItems[7].link or nil
            local enchantReceived = _staging.targetItems[7] and _staging.targetItems[7].link or nil

            -- Don't log truly empty trades (no items either side, no money).
            -- Common when players open a trade window to inspect each other.
            if #given == 0 and #received == 0
               and (_staging.playerMoney or 0) == 0
               and (_staging.targetMoney or 0) == 0
               and not enchantGiven and not enchantReceived then
                _staging = nil
                return
            end

            local partner = "?"
            if UnitName then
                -- The trade partner is `npc` (the unit type for the player
                -- on the other side of a window). Realm comes from a second
                -- return on connected-realm clusters; default to ours if
                -- not provided (most common case).
                local n, r = UnitName("npc")
                if n then
                    partner = (r and r ~= "") and (n .. "-" .. r) or n
                end
            end

            tinsert(db.entries, {
                ts              = GetServerTime() or time(),
                player          = CharKey(),
                other           = partner,
                given           = given,
                received        = received,
                moneyGiven      = _staging.playerMoney or 0,
                moneyReceived   = _staging.targetMoney or 0,
                enchantGiven    = enchantGiven,
                enchantReceived = enchantReceived,
                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
            _staging = nil
            refreshSubTab()
        end
    end
end)

-- ---------------------------------------------------------------------------
-- Read API consumed by GUI/TradeLogSubTab.lua
-- ---------------------------------------------------------------------------
function TradeLog:GetEntries(filterPlayer, fromTs, toTs)
    local db = TradeLogDB()
    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 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

function TradeLog:GetPlayerKeys()
    local db = TradeLogDB()
    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 TradeLog:ClearAll()
    local db = TradeLogDB()
    if db then db.entries = {} end
    refreshSubTab()
end
