local addonName, NXR = ...

-- ============================================================================
-- Module-local state
-- ============================================================================

local snapshot          = {}   -- bracketIndex → rating from NelxRatedDB before match
local pendingEnemySpecs = {}  -- populated by ARENA_PREP_OPPONENT_SPECIALIZATIONS
local pendingAllySpecs  = {}  -- teammate specs captured at ARENA_PREP (best-effort, inspect cache)
local pendingRecord    = nil  -- partial record held between PVP_MATCH_COMPLETE and PVP_RATED_STATS_UPDATE

-- Solo Shuffle per-round tracking
local ssRounds        = {}    -- accumulated per-round records: { num, outcome, duration }
local ssRoundStart    = nil   -- GetTime() at state-3 onset for current round
local ssRoundPrevWins = 0     -- wins snapshot taken at round start
local ssActive        = false -- true only inside a confirmed SS match
local matchBracketHint = nil  -- bracket captured early as fallback for DB-diff detection

NXR.InsightsDebug = false

-- ============================================================================
-- Public accessor (I-5)
-- ============================================================================

function NXR.GetMatches()
    return NelxRatedDB.matches or {}
end

-- Only callable with InsightsDebug=true.
-- Removes records with no bracket or no rating.
-- For SS records: clears shuffle.rounds if captured rounds < 6 (keeps match-level data).
function NXR.PurgeCorruptMatches()
    if not NXR.InsightsDebug then
        print("[NXR] PurgeCorruptMatches requires InsightsDebug=true")
        return
    end
    local matches = NelxRatedDB.matches
    if not matches then print("[NXR] No match data."); return end

    local kept, removed, fixed = {}, 0, 0
    for _, r in ipairs(matches) do
        if not r.bracketIndex or not r.rating then
            removed = removed + 1
        else
            if r.shuffle and r.shuffle.rounds and #r.shuffle.rounds < 6 then
                r.shuffle.rounds = {}
                fixed = fixed + 1
            end
            kept[#kept + 1] = r
        end
    end

    NelxRatedDB.matches = kept
    print(("[NXR] Purge complete — removed %d, fixed %d SS round tables, kept %d"):format(
        removed, fixed, #kept))
end

-- ============================================================================
-- Internal helpers
-- ============================================================================

-- Identify the bracket of the currently active rated match using live PvP API.
-- Priority: SS/Blitz boolean checks, then arena opponent count (most authoritative
-- for 2v2 vs 3v3), then GetBattlefieldStatus.teamSize as last resort.
-- Returns nil if no active rated match identifiable.
local function DetectActiveBracket()
    if C_PvP and C_PvP.IsSoloShuffle and C_PvP.IsSoloShuffle() then
        return NXR.BRACKET_SOLO_SHUFFLE
    end
    if C_PvP and C_PvP.IsRatedSoloRBG and C_PvP.IsRatedSoloRBG() then
        return NXR.BRACKET_BLITZ
    end
    if IsActiveBattlefieldArena and IsActiveBattlefieldArena() then
        local opp = (GetNumArenaOpponents and GetNumArenaOpponents()) or 0
        if opp == 2 then return NXR.BRACKET_2V2 end
        if opp == 3 then return NXR.BRACKET_3V3 end
        local maxId = (GetMaxBattlefieldID and GetMaxBattlefieldID()) or 10
        for i = 1, maxId do
            local status, _, teamSize = GetBattlefieldStatus(i)
            if status == "active" then
                if teamSize == 2 then return NXR.BRACKET_2V2 end
                if teamSize == 3 then return NXR.BRACKET_3V3 end
            end
        end
    end
    return nil
end

-- Read current rating from DB for a given char/bracket.
local function GetDBRating(charKey, bi)
    local char = NelxRatedDB
        and NelxRatedDB.characters
        and NelxRatedDB.characters[charKey]
    if not char then return nil end
    local data
    if NXR.PER_SPEC_BRACKETS[bi] then
        local specID = char.specID
        if specID and char.specBrackets and char.specBrackets[specID] then
            data = char.specBrackets[specID][bi]
        end
    elseif char.brackets then
        data = char.brackets[bi]
    end
    return data and data.rating
end

-- Read current saved ratings from NelxRatedDB into snapshot.
-- No WoW PvP API calls — safe at any time, including during zone transitions.
local function TakeDBSnapshot(charKey)
    local char = charKey
        and NelxRatedDB
        and NelxRatedDB.characters
        and NelxRatedDB.characters[charKey]
    snapshot = {}
    if not char then
        NXR.DebugInsights("TakeDBSnapshot: no char data for", tostring(charKey))
        return
    end
    for _, bi in ipairs(NXR.TRACKED_BRACKETS) do
        local data
        if NXR.PER_SPEC_BRACKETS[bi] then
            local specID = char.specID
            if specID and char.specBrackets and char.specBrackets[specID] then
                data = char.specBrackets[specID][bi]
            end
        else
            if char.brackets then
                data = char.brackets[bi]
            end
        end
        snapshot[bi] = data and data.rating
    end
    NXR.DebugInsights("TakeDBSnapshot:",
        "2v2=" .. tostring(snapshot[NXR.BRACKET_2V2]),
        "3v3=" .. tostring(snapshot[NXR.BRACKET_3V3]),
        "blitz=" .. tostring(snapshot[NXR.BRACKET_BLITZ]),
        "ss=" .. tostring(snapshot[NXR.BRACKET_SOLO_SHUFFLE]))
end

-- Compare current NelxRatedDB ratings vs snapshot to find the bracket that changed.
-- Called one frame after PVP_RATED_STATS_UPDATE so Core.lua has already written new values.
local function DetectBracketFromDB(charKey)
    local char = charKey
        and NelxRatedDB
        and NelxRatedDB.characters
        and NelxRatedDB.characters[charKey]
    if not char then return nil end

    for _, bi in ipairs(NXR.TRACKED_BRACKETS) do
        local prev = snapshot[bi]
        if prev ~= nil then
            local data
            if NXR.PER_SPEC_BRACKETS[bi] then
                local specID = char.specID
                if specID and char.specBrackets and char.specBrackets[specID] then
                    data = char.specBrackets[specID][bi]
                end
            else
                if char.brackets then
                    data = char.brackets[bi]
                end
            end

            local current = data and data.rating
            if current ~= nil and current ~= prev then
                NXR.DebugInsights("DetectBracketFromDB: bracket", bi,
                    "changed", prev, "->", current)
                return bi
            end
        end
    end
    return nil
end

-- Lazy-built lookup: classToken + lower(specName) -> specID
-- C_PvP.GetScoreInfo().talentSpec is a localized spec NAME string, not an ID.
local specLookup
local function BuildSpecLookup()
    specLookup = {}
    local numClasses = (GetNumClasses and GetNumClasses()) or 0
    for ci = 1, numClasses do
        local _, classToken, classID = GetClassInfo(ci)
        if classToken and classID then
            local numSpecs = 0
            if C_SpecializationInfo and C_SpecializationInfo.GetNumSpecializationsForClassID then
                numSpecs = C_SpecializationInfo.GetNumSpecializationsForClassID(classID) or 0
            end
            for si = 1, numSpecs do
                if GetSpecializationInfoForClassID then
                    local specID, specName = GetSpecializationInfoForClassID(classID, si)
                    if specID and specName then
                        specLookup[classToken:upper() .. "_" .. specName:lower()] = specID
                    end
                end
            end
        end
    end
end

local function ResolveSpecID(classToken, talentSpecName)
    if not classToken or not talentSpecName or talentSpecName == "" then return nil end
    if not specLookup then BuildSpecLookup() end
    return specLookup[classToken:upper() .. "_" .. tostring(talentSpecName):lower()]
end

local function IsSecret(v)
    return v ~= nil and issecretvalue and issecretvalue(v)
end

-- Safe split: name "X-Realm" -> "X","Realm". Tainted/secret values guarded.
local function SplitName(full)
    if not full or IsSecret(full) then return nil, nil end
    local ok, n, r = pcall(function()
        local nm, rl = full:match("^(.+)-(.+)$")
        return nm, rl
    end)
    if not ok then return nil, nil end
    return n or full, r
end

-- Pull rating, MMR, specs, and ally/enemy split entirely from scoreboard.
-- Mirrors the working ArenaHistoryAnalytics approach: combine C_PvP.GetScoreInfo
-- with legacy GetBattlefieldScore for faction; resolve specID from talentSpec name;
-- fallback MMR to GetBattlefieldTeamInfo for arena 2v2/3v3.
-- Returns true on success (self row found and parsed).
local function CaptureFromScoreboard(rec)
    if not C_PvP or not C_PvP.GetScoreInfo then return false end
    if RequestBattlefieldScoreData then RequestBattlefieldScoreData() end
    local n = (GetNumBattlefieldScores and GetNumBattlefieldScores()) or 0
    if n == 0 then return false end

    local _, playerRealm = UnitFullName("player")
    local playerName     = UnitName("player")
    local playerFull     = (playerName and playerRealm and playerRealm ~= "")
        and (playerName .. "-" .. playerRealm) or playerName

    local entries  = {}
    local selfRow

    for i = 1, n do
        local info = C_PvP.GetScoreInfo(i)
        local bfName, bfKB, _, bfDeaths, _, bfFac, _, _, bfClass, bfDmg, bfHeal = GetBattlefieldScore(i)
        local rawName    = (info and info.name) or bfName
        if rawName then
            -- Append player's realm to bare names (cross-realm matchup data has full name)
            local name = rawName
            if not IsSecret(name) and not name:find("-", 1, true) and playerRealm and playerRealm ~= "" then
                name = name .. "-" .. playerRealm
            end
            local shortName, realm = SplitName(name)
            local classToken = (info and info.classToken) or bfClass or ""
            local talentSpec = info and info.talentSpec
            local specID     = ResolveSpecID(classToken, talentSpec)
            local faction    = tonumber((info and info.faction) or bfFac) or -1
            local rating       = tonumber(info and info.rating) or 0
            local ratingChange = tonumber(info and info.ratingChange) or 0
            local preMMR       = tonumber(info and info.prematchMMR) or 0
            local postMMR      = tonumber(info and info.postmatchMMR) or 0
            local mmrChange    = (preMMR > 0 and postMMR > 0) and (postMMR - preMMR)
                or (tonumber(info and info.mmrChange) or 0)

            local isSelf = (info and info.isSelf)
                or (shortName and shortName == playerName)
                or (name == playerFull)

            if NXR.InsightsDebug then
                print("[NXR Insights] score[" .. i .. "]"
                    .. " name=" .. tostring(name)
                    .. " classToken=" .. tostring(classToken)
                    .. " talentSpec=" .. tostring(talentSpec)
                    .. " specID=" .. tostring(specID)
                    .. " faction=" .. tostring(faction)
                    .. " isSelf=" .. tostring(isSelf)
                    .. " rating=" .. tostring(rating)
                    .. " ratingChange=" .. tostring(ratingChange)
                    .. " prematchMMR=" .. tostring(preMMR)
                    .. " postmatchMMR=" .. tostring(postMMR))
            end

            local row = {
                name        = shortName or name,
                realm       = realm,
                charKey     = (shortName and realm) and (shortName .. "-" .. realm) or nil,
                classToken  = classToken,
                specID      = specID,
                faction     = faction,
                isSelf      = isSelf,
                rating      = rating,
                ratingChange= ratingChange,
                prematchMMR = preMMR,
                mmrChange   = mmrChange,
            }
            -- SS: stats[1].pvpStatValue is round-win count
            if info and info.stats and info.stats[1]
                and type(info.stats[1].pvpStatValue) == "number" then
                row.roundsWon = info.stats[1].pvpStatValue
            end
            entries[#entries + 1] = row
            if isSelf then selfRow = row end
        end
    end

    if not selfRow then return false end

    rec.rating       = selfRow.rating
    rec.ratingChange = selfRow.ratingChange
    rec.prematchMMR  = selfRow.prematchMMR
    rec.mmrChange    = selfRow.mmrChange
    if selfRow.roundsWon ~= nil then rec.wonRounds = selfRow.roundsWon end

    -- Partition by faction (arena team index 0/1).
    local myFac = selfRow.faction
    local allies, enemies = {}, {}
    for _, row in ipairs(entries) do
        if not row.isSelf then
            if myFac ~= -1 and row.faction == myFac then
                allies[#allies + 1] = row
            elseif myFac ~= -1 and row.faction ~= -1 then
                enemies[#enemies + 1] = row
            end
        end
    end

    -- MMR fallback for arena 2v2/3v3: per-player prematchMMR is often 0;
    -- GetBattlefieldTeamInfo returns team-level MMR.
    if (rec.prematchMMR == nil or rec.prematchMMR == 0)
        and GetBattlefieldTeamInfo
        and GetBattlefieldArenaFaction
    then
        local myArenaFac = tonumber(GetBattlefieldArenaFaction()) or 0
        local _, _, _, myMMR  = GetBattlefieldTeamInfo(myArenaFac)
        myMMR = tonumber(myMMR)
        if myMMR and myMMR > 0 then
            rec.prematchMMR = myMMR
        end
    end

    -- Build legacy spec-id arrays + new player-info arrays.
    -- Only overwrite existing rec.* if scoreboard partition yielded entries
    -- (preserves ARENA_PREP fallback when scoreboard partition fails).
    if #allies > 0 then
        local allySpecs, allyPlayers = {}, {}
        for _, row in ipairs(allies) do
            allySpecs[#allySpecs + 1] = row.specID or 0
            allyPlayers[#allyPlayers + 1] = {
                name = row.name, realm = row.realm, charKey = row.charKey,
                classToken = row.classToken, specID = row.specID,
                prematchMMR = row.prematchMMR, mmrChange = row.mmrChange,
                rating = row.rating, ratingChange = row.ratingChange,
            }
        end
        rec.allySpecs   = allySpecs
        rec.allyPlayers = allyPlayers
    end
    if #enemies > 0 then
        local enemySpecs, enemyPlayers = {}, {}
        for _, row in ipairs(enemies) do
            enemySpecs[#enemySpecs + 1] = row.specID or 0
            enemyPlayers[#enemyPlayers + 1] = {
                name = row.name, realm = row.realm, charKey = row.charKey,
                classToken = row.classToken, specID = row.specID,
                prematchMMR = row.prematchMMR, mmrChange = row.mmrChange,
                rating = row.rating, ratingChange = row.ratingChange,
            }
        end
        rec.enemySpecs   = enemySpecs
        rec.enemyPlayers = enemyPlayers
        rec.opponentCount = #enemies
    end

    rec.scoreLoaded = true
    return true
end

-- Read the player's current Solo Shuffle round-win count from the scoreboard.
-- Caller should invoke RequestBattlefieldScoreData() before this if available.
-- Returns a number (0 on miss) — never nil, safe to compare with ssRoundPrevWins.
local function GetMyCurrentWins()
    if not C_PvP or not C_PvP.GetScoreInfo then return 0 end
    local playerName     = UnitName("player")
    local playerFullName = playerName and (playerName .. "-" .. GetRealmName()) or nil
    local n              = (GetNumBattlefieldScores and GetNumBattlefieldScores()) or 0
    for i = 1, n do
        local info = C_PvP.GetScoreInfo(i)
        if not info then break end
        if info.isSelf or info.name == playerName or info.name == playerFullName then
            if info.stats and info.stats[1] and type(info.stats[1].pvpStatValue) == "number" then
                return info.stats[1].pvpStatValue
            end
            return 0
        end
    end
    return 0
end

-- ============================================================================
-- Event frame
-- ============================================================================

local insightsFrame = CreateFrame("Frame")
insightsFrame:RegisterEvent("ADDON_LOADED")

insightsFrame:SetScript("OnEvent", function(self, event, ...)
    -- ---- I-1: Bootstrap after addon loads ----
    if event == "ADDON_LOADED" then
        local loadedAddon = ...
        if loadedAddon ~= addonName then return end
        self:UnregisterEvent("ADDON_LOADED")
        self:RegisterEvent("PVP_MATCH_ACTIVE")
        self:RegisterEvent("PLAYER_LEAVING_WORLD")
        self:RegisterEvent("ARENA_PREP_OPPONENT_SPECIALIZATIONS")
        self:RegisterEvent("PVP_MATCH_STATE_CHANGED")
        self:RegisterEvent("PVP_MATCH_COMPLETE")
        self:RegisterEvent("UPDATE_BATTLEFIELD_SCORE")
        self:RegisterEvent("PVP_RATED_STATS_UPDATE")

    -- ---- SS match start: init per-round state ----
    elseif event == "PVP_MATCH_ACTIVE" then
        local isSS = C_PvP and C_PvP.IsSoloShuffle and C_PvP.IsSoloShuffle()
        -- SS fires PVP_MATCH_ACTIVE on every round zone-in; IsSoloShuffle() returns false
        -- during the zone transition. If we already confirmed this is SS (hint set from a
        -- prior round's state-change recovery), preserve accumulated rounds and re-arm.
        if not isSS and matchBracketHint == NXR.BRACKET_SOLO_SHUFFLE then
            ssActive = true
            NXR.DebugInsights("PVP_MATCH_ACTIVE: SS round zone-in, preserving state rounds=", #ssRounds)
            return
        end
        ssActive           = isSS and true or false
        matchBracketHint   = DetectActiveBracket() or (isSS and NXR.BRACKET_SOLO_SHUFFLE or nil)
        ssRounds           = {}
        ssRoundStart       = nil
        ssRoundPrevWins    = 0
        NXR.DebugInsights("PVP_MATCH_ACTIVE isSS=", tostring(ssActive))

    -- ---- I-2: DB snapshot before zone transition (no API restriction risk) ----
    elseif event == "PLAYER_LEAVING_WORLD" then
        TakeDBSnapshot(NXR.currentCharKey)
        if ssActive then
            -- SS zones between every round — preserve accumulated ssRounds across zone-outs.
            -- Only clear per-round timing; ssActive re-armed at next state=3.
            ssRoundStart = nil
            ssActive     = false
            NXR.DebugInsights("PLAYER_LEAVING_WORLD: SS inter-round zone, preserving", #ssRounds, "rounds")
        end

    -- ---- I-3: Enemy spec capture ----
    elseif event == "ARENA_PREP_OPPONENT_SPECIALIZATIONS" then
        pendingEnemySpecs = {}
        local count = GetNumArenaOpponentSpecs()
        for i = 1, count do
            local specID = GetArenaOpponentSpec(i)
            pendingEnemySpecs[i] = (specID and specID ~= 0) and specID or 0
        end
        NXR.DebugInsights("enemy specs captured, count=", count)
        -- Authoritative bracket signal for arena: opponent count maps directly to size
        if count == 2 then
            matchBracketHint = NXR.BRACKET_2V2
        elseif count == 3 then
            matchBracketHint = NXR.BRACKET_3V3
        end
        -- Ally specs: best-effort — inspect cache may not be populated at prep time
        pendingAllySpecs = {}
        for i = 1, 4 do
            local tok = "party" .. i
            if UnitExists(tok) then
                local sid = GetInspectSpecialization and GetInspectSpecialization(tok)
                pendingAllySpecs[#pendingAllySpecs + 1] = (sid and sid ~= 0) and sid or nil
            end
        end
        NXR.DebugInsights("ally specs captured, count=", #pendingAllySpecs)
        -- Refresh snapshot in case PLAYER_LEAVING_WORLD missed the char
        if not snapshot[NXR.BRACKET_SOLO_SHUFFLE] and not snapshot[NXR.BRACKET_2V2] then
            TakeDBSnapshot(NXR.currentCharKey)
        end

    -- ---- SS round tracking via match state transitions ----
    elseif event == "PVP_MATCH_STATE_CHANGED" then
        local newState = C_PvP and C_PvP.GetActiveMatchState and C_PvP.GetActiveMatchState()
        newState = tonumber(newState)
        if not newState then return end

        local liveSS = C_PvP and C_PvP.IsSoloShuffle and C_PvP.IsSoloShuffle()
        NXR.DebugInsights("PVP_MATCH_STATE_CHANGED state=", newState,
            "ssActive=", tostring(ssActive), "liveSS=", tostring(liveSS),
            "rounds so far=", #ssRounds)

        -- IsSoloShuffle() can return false at PVP_MATCH_ACTIVE time — check live as fallback
        if not ssActive then
            if liveSS then
                ssActive         = true
                matchBracketHint = NXR.BRACKET_SOLO_SHUFFLE
            else
                return
            end
        end

        if newState == 2 then
            -- Enum.PvPMatchState.Engaged (Midnight: 2) — round starting
            ssRoundStart = GetTime()
            if RequestBattlefieldScoreData then RequestBattlefieldScoreData() end
            C_Timer.After(0.2, function()
                ssRoundPrevWins = GetMyCurrentWins()
                NXR.DebugInsights("Round start wins snapshot:", ssRoundPrevWins)
            end)

        elseif ssRoundStart ~= nil then
            -- Any non-Engaged state while a round was active = round ended.
            -- Avoids hardcoding PostRound value (3? 4?) which varies by build.
            local capturedStart = ssRoundStart
            if not capturedStart then
                NXR.DebugInsights("state", newState, "but no ssRoundStart — skipping")
                return
            end

            ssRoundStart = nil  -- clear immediately to prevent double-capture

            local roundNum  = #ssRounds + 1
            local duration  = math.floor(GetTime() - capturedStart)
            local prevWins  = ssRoundPrevWins

            if roundNum <= 6 then
                -- Insert placeholder; outcome resolved after scoreboard delay
                local roundEntry = { num = roundNum, outcome = "unknown", duration = duration }
                ssRounds[roundNum] = roundEntry

                C_Timer.After(0.6, function()
                    if RequestBattlefieldScoreData then RequestBattlefieldScoreData() end
                    C_Timer.After(0.2, function()
                        local newWins = GetMyCurrentWins()
                        roundEntry.outcome  = newWins > prevWins and "win" or "loss"
                        ssRoundPrevWins     = newWins
                        NXR.DebugInsights("Round", roundNum, "outcome:", roundEntry.outcome,
                            "wins:", prevWins, "->", newWins)
                    end)
                end)
            else
                NXR.DebugInsights("roundNum > 6, skipping (roundNum=", roundNum, ")")
            end
        end

    -- ---- I-4 Stage 1: Stash partial record ----
    elseif event == "PVP_MATCH_COMPLETE" then
        -- Match is over — no more rounds will start; stop processing state changes
        ssActive = false

        local winner, duration = ...

        if NXR.InsightsDebug then
            print("[NXR Insights] PVP_MATCH_COMPLETE winner=" .. tostring(winner)
                .. " duration=" .. tostring(duration))
            local _, iType, _, iName = GetInstanceInfo()
            print("[NXR Insights] instance type=" .. tostring(iType) .. " name=" .. tostring(iName))
            for _, bi in ipairs(NXR.TRACKED_BRACKETS) do
                print("[NXR Insights] bracket " .. bi .. " snapshot=" .. tostring(snapshot[bi]))
            end
        end

        if not NXR.currentCharKey then
            NXR.DebugInsights("no currentCharKey, skipping")
            return
        end

        local charKey = NXR.currentCharKey
        local specID
        local char = NelxRatedDB.characters[charKey]
        if char then specID = char.specID end

        -- Refresh hint at completion in case PVP_MATCH_ACTIVE detection missed
        -- (e.g. C_PvP.IsSoloShuffle returning false during zone transition).
        matchBracketHint = matchBracketHint or DetectActiveBracket()

        -- Derive outcome directly from team index: 0=purple, 1=gold.
        -- GetBattlefieldArenaFaction() returns which team the player is on.
        -- No draws in 2v2/3v3; this field is ignored for SS (wonRounds used instead).
        local directOutcome
        if type(winner) == "number" then
            local myTeam = GetBattlefieldArenaFaction()
            if type(myTeam) == "number" then
                directOutcome = (winner == myTeam) and "win" or "loss"
            end
        end

        pendingRecord = {
            timestamp     = time(),
            charKey       = charKey,
            specID        = specID,
            enemySpecs    = pendingEnemySpecs,
            allySpecs     = pendingAllySpecs,
            bracketHint   = matchBracketHint,
            directOutcome = directOutcome,
        }

        pendingEnemySpecs = {}
        pendingAllySpecs  = {}
        NXR.DebugInsights("Stage 1 complete — charKey=", charKey)

    -- ---- I-4 Stage 2: Accumulate score data (best-effort) ----
    elseif event == "UPDATE_BATTLEFIELD_SCORE" then
        if not pendingRecord or pendingRecord.scoreLoaded then return end
        CaptureFromScoreboard(pendingRecord)
        if not pendingRecord.scoreLoaded then
            NXR.DebugInsights("score not found in UPDATE_BATTLEFIELD_SCORE, will retry")
        end

    -- ---- I-4 Stage 3: Finalize one frame after Core.lua writes new ratings ----
    elseif event == "PVP_RATED_STATS_UPDATE" then
        if not pendingRecord then return end

        local rec = pendingRecord
        pendingRecord = nil  -- clear now; timer callback captures rec

        -- Delay finalize ~1.5s: per-player scoreboard MMR fields and faction
        -- partitioning aren't reliably populated immediately after
        -- PVP_RATED_STATS_UPDATE. ArenaHistoryAnalytics uses the same delay.
        C_Timer.After(1.5, function()
            -- Final scoreboard read (always re-capture; scoreboard now stable)
            CaptureFromScoreboard(rec)

            -- Bracket detection priority:
            --   1. Scoreboard opponent count (2 = 2v2, 3 = 3v3) — most reliable for arena
            --   2. SS / Blitz boolean checks (don't rely on opponent count)
            --   3. DB rating diff
            --   4. ARENA_PREP-time hint
            --   5. Live-API DetectActiveBracket() at finalize
            local function bracketFromScoreboard()
                if C_PvP and C_PvP.IsSoloShuffle and C_PvP.IsSoloShuffle() then
                    return NXR.BRACKET_SOLO_SHUFFLE
                end
                if C_PvP and C_PvP.IsRatedSoloRBG and C_PvP.IsRatedSoloRBG() then
                    return NXR.BRACKET_BLITZ
                end
                if rec.opponentCount == 2 then return NXR.BRACKET_2V2 end
                if rec.opponentCount == 3 then return NXR.BRACKET_3V3 end
                return nil
            end

            rec.bracketIndex = bracketFromScoreboard()
                or DetectBracketFromDB(rec.charKey)
                or rec.bracketHint
                or DetectActiveBracket()
            rec.bracketHint   = nil
            rec.opponentCount = nil
            NXR.DebugInsights("bracket detected —", tostring(rec.bracketIndex))

            -- Authoritative rating + ratingChange from DB diff: scoreboard sometimes
            -- returns 0/nil ratingChange (mislabels losses as draws). DB has ground truth.
            if rec.bracketIndex then
                local curRating = GetDBRating(rec.charKey, rec.bracketIndex)
                local prev      = snapshot[rec.bracketIndex]
                if curRating then
                    -- DB has authoritative post-match rating (written by Core from
                    -- C_PvP.GetRatedBracketInfo on PVP_RATED_STATS_UPDATE). Scoreboard
                    -- rating field returns stale data in Midnight 12.x — overwrite.
                    rec.rating = curRating
                    if prev then
                        local diff = curRating - prev
                        if diff ~= 0 and (rec.ratingChange == nil or rec.ratingChange == 0) then
                            rec.ratingChange = diff
                            NXR.DebugInsights("ratingChange overridden from DB diff:", diff)
                        end
                    end
                end
            end

            -- Derive outcome:
            --   SS  → wonRounds (>3 win, <3 loss, ==3 draw), fallback to ratingChange
            --   2v2/3v3/Blitz → directOutcome from PVP_MATCH_COMPLETE winner arg,
            --                   fallback to ratingChange sign
            if rec.bracketIndex == NXR.BRACKET_SOLO_SHUFFLE then
                local wr = rec.wonRounds
                if type(wr) == "number" then
                    if wr > 3 then
                        rec.outcome = "win"
                    elseif wr < 3 then
                        rec.outcome = "loss"
                    else
                        rec.outcome = "draw"
                    end
                else
                    NXR.DebugInsights("SS outcome fallback to ratingChange (wonRounds nil)")
                    local rc = rec.ratingChange
                    if rc == nil then
                        rec.outcome = "unknown"
                    elseif rc > 0 then
                        rec.outcome = "win"
                    elseif rc < 0 then
                        rec.outcome = "loss"
                    else
                        rec.outcome = "draw"
                    end
                end
            elseif rec.directOutcome then
                rec.outcome = rec.directOutcome
                NXR.DebugInsights("outcome from winner arg:", rec.outcome)
            else
                NXR.DebugInsights("outcome fallback to ratingChange sign")
                local rc = rec.ratingChange
                if rc == nil then
                    rec.outcome = "unknown"
                elseif rc > 0 then
                    rec.outcome = "win"
                else
                    rec.outcome = "loss"
                end
            end
            rec.directOutcome = nil

            -- SS shuffle data: trust scoreboard totals (reliable), only include
            -- rounds[] breakdown when state-change tracking captured all 6
            -- (per-round states don't fire reliably in Midnight 12.x).
            if rec.bracketIndex == NXR.BRACKET_SOLO_SHUFFLE then
                local won   = rec.wonRounds or 0
                local total = 6
                rec.shuffle = {
                    wonRounds   = won,
                    lostRounds  = total - won,
                    totalRounds = total,
                }
                if #ssRounds == total then
                    local capturedRounds = {}
                    for i = 1, total do
                        capturedRounds[i] = ssRounds[i]
                    end
                    rec.shuffle.rounds = capturedRounds
                    NXR.DebugInsights("shuffle: full per-round capture (", total, "rounds)")
                else
                    NXR.DebugInsights("shuffle: partial capture (", #ssRounds, "/", total,
                        ") — omitting rounds[], totals only")
                end
                ssRounds        = {}
                ssRoundStart    = nil
                ssRoundPrevWins = 0
                ssActive        = false
            end

            rec.scoreLoaded = nil  -- don't persist internal flag

            NelxRatedDB.matches[#NelxRatedDB.matches + 1] = rec
            NXR.DebugInsights("match recorded — bracket=", tostring(rec.bracketIndex),
                "outcome=", rec.outcome,
                "rating=", tostring(rec.rating))

            snapshot = {}
        end)
    end
end)
