-- 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(p,t)         on the (1,1) "both accepted" edge,
--                                      snapshot the slots and FREEZE a copy
--                                      (the completion latch + final data).
--                                      Not cleared by a later reset.
--   * TRADE_CLOSED                     commit the frozen copy if present (the
--                                      completion signal on Retail — its
--                                      trade frame no longer surfaces
--                                      ERR_TRADE_COMPLETE). TRADE_CLOSED fires
--                                      for both completion AND cancel, so a
--                                      frozen copy (= reached mutual-accept)
--                                      is what marks it completed. Retail
--                                      fires a teardown (0,0) and empties the
--                                      slots just before TRADE_CLOSED, which
--                                      is exactly why the copy is frozen at
--                                      (1,1) and not re-read here.
--   * UI_INFO_MESSAGE → ERR_TRADE_COMPLETE  Classic completion signal;
--                                      commits the same frozen copy. A
--                                      `_committed` flag dedupes when both it
--                                      and TRADE_CLOSED fire.
--
-- 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 completion (TRADE_CLOSED after both
-- sides accepted, or ERR_TRADE_COMPLETE on Classic), discarded otherwise.
-- Two slot-keyed maps (1-6 items + 7 enchant) keep the last-known state of
-- each slot regardless of update order.
-- ---------------------------------------------------------------------------
local _staging = nil
-- Frozen copy of the trade contents taken at the instant BOTH sides have
-- accepted (TRADE_ACCEPT_UPDATE with playerAccepted==1 AND targetAccepted==1).
-- Its mere presence is the "this trade reached mutual-accept" latch, AND it
-- holds the final items/money/partner as an independent snapshot.
--
-- Why a frozen latch instead of reading live state at TRADE_CLOSED:
-- on Retail a completing trade fires a final TRADE_ACCEPT_UPDATE(0,0) AND
-- empties the trade slots (TRADE_*_ITEM_CHANGED) just before TRADE_CLOSED.
-- The old code tracked both-accepted as a live boolean and re-snapshotted on
-- every accept-update, so that teardown (0,0) both cleared the flag (→ no
-- commit) and overwrote the captured items with empties. Latching + freezing
-- at the (1,1) moment is immune to the teardown: the data is copied out then,
-- and a later reset can't un-latch it. If the user un-accepts to edit and
-- re-accepts, the next (1,1) overwrites the frozen copy with the new contents.
local _completed = nil
-- Set once a trade is logged, to dedupe the two completion signals that can
-- both fire on Classic (ERR_TRADE_COMPLETE then TRADE_CLOSED).
local _committed = false

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
    -- Capture the partner name NOW, while the trade window is open and the
    -- "npc" unit is valid. On Retail we commit at TRADE_CLOSED, by which
    -- point the "npc" unit can already be gone — reading it there would log
    -- every trade's partner as "?". Refreshed on every snapshot (TRADE_SHOW
    -- + each TRADE_ACCEPT_UPDATE) so the last valid read is the one kept.
    if UnitName then
        local n, r = UnitName("npc")
        if n then
            _staging.partner = (r and r ~= "") and (n .. "-" .. r) or n
        end
    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

-- Build a frozen, self-contained copy of the current staging — independent
-- tables (slotsToList allocates fresh) and immutable values, so later slot
-- teardown can't mutate it. Returns nil if there's nothing staged.
local function freezeStaging()
    if not _staging then return nil end
    return {
        given           = slotsToList(_staging.playerItems),
        received        = slotsToList(_staging.targetItems),
        enchantGiven    = _staging.playerItems[7] and _staging.playerItems[7].link or nil,
        enchantReceived = _staging.targetItems[7] and _staging.targetItems[7].link or nil,
        moneyGiven      = _staging.playerMoney or 0,
        moneyReceived   = _staging.targetMoney or 0,
        partner         = _staging.partner,
    }
end

-- Commit a frozen trade snapshot to db.entries. `data` is the value produced
-- by freezeStaging() at mutual-accept. Deduped via `_committed` so the two
-- completion signals that can both fire on Classic (ERR_TRADE_COMPLETE then
-- TRADE_CLOSED) only log once.
local function commitTrade(data)
    if not (data and isEnabled()) or _committed then return end

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

    local db = TradeLogDB()
    if not db then return end
    db.entries = db.entries or {}

    tinsert(db.entries, {
        ts              = GetServerTime() or time(),
        player          = CharKey(),
        -- Partner captured by snapshotAllSlots() while the window was open
        -- (the "npc" unit), not read live at commit — by TRADE_CLOSED on
        -- Retail the "npc" unit may already be gone.
        other           = data.partner or "?",
        given           = data.given,
        received        = data.received,
        moneyGiven      = data.moneyGiven or 0,
        moneyReceived   = data.moneyReceived or 0,
        enchantGiven    = data.enchantGiven,
        enchantReceived = data.enchantReceived,
        zone            = (GetRealZoneText and GetRealZoneText()) or nil,
    })
    _committed = true

    addon:Debug(string.format(
        "[trade] LOGGED: other=%s given=%d received=%d moneyG=%d moneyR=%d",
        tostring(data.partner), #data.given, #data.received,
        data.moneyGiven or 0, data.moneyReceived or 0))

    local Logs = addon.modules and addon.modules["logs"]
    if Logs and Logs.PruneEntries then Logs:PruneEntries(db.entries) end
    refreshSubTab()
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()
        _completed = nil
        _committed = false
        snapshotAllSlots()
        addon:Debug("[trade] TRADE_SHOW — staging opened")

    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
        if not _staging then return end
        -- args: (playerAccepted, targetAccepted), each 1 or 0. Only the
        -- (1,1) "both locked in" edge matters: snapshot the final contents
        -- and FREEZE them. We deliberately do NOT clear the latch on a
        -- later (0,0)/(1,0) — a completing trade on Retail fires a teardown
        -- (0,0) (and empties the slots) right before TRADE_CLOSED, and we
        -- must survive that. A genuine edit-then-re-accept produces a new
        -- (1,1) that re-freezes with the updated contents.
        local playerAccepted, targetAccepted = ...
        addon:Debug(string.format("[trade] TRADE_ACCEPT_UPDATE p=%s t=%s",
            tostring(playerAccepted), tostring(targetAccepted)))
        if playerAccepted == 1 and targetAccepted == 1 then
            snapshotAllSlots()
            _completed = freezeStaging()
            addon:Debug("[trade]   both accepted — froze trade snapshot")
        end

    elseif event == "TRADE_CLOSED" then
        -- TRADE_CLOSED fires for BOTH completion and cancellation; Blizzard's
        -- own frame can't tell them apart either. A frozen `_completed`
        -- snapshot means the trade reached mutual-accept, which we treat as
        -- completed. This is the only completion signal on Retail (no
        -- ERR_TRADE_COMPLETE there). If ERR_TRADE_COMPLETE already logged on
        -- Classic, `_committed` makes this a no-op.
        addon:Debug(string.format("[trade] TRADE_CLOSED — completed=%s committed=%s",
            tostring(_completed ~= nil), tostring(_committed)))
        if _completed then commitTrade(_completed) end
        _staging   = nil
        _completed = nil
        _committed = false

    elseif event == "UI_INFO_MESSAGE" then
        -- Classic's "Trade complete." confirmation, which fires BEFORE the
        -- slot teardown, so the live staging is still valid here. Prefer the
        -- frozen copy if we have one, else freeze the live staging now.
        local _, msg = ...
        if msg == ERR_TRADE_COMPLETE then
            addon:Debug("[trade] UI_INFO_MESSAGE ERR_TRADE_COMPLETE (Classic)")
            commitTrade(_completed or freezeStaging())
        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
