-- ============================================================
-- FILE: MidnightPI_Captures.lua
-- PURPOSE: Records the Blizzard events that the MPI server-side
--   pull-detection system needs but isn't already getting from
--   the combat log alone. Captured during M+ keys and shipped
--   alongside the combat log on next upload.
-- HOW IT WORKS:
--   1. Listens for the dungeon-start signal. Resets the capture
--      buckets to empty so each run starts fresh.
--   2. Records five things while the key is in progress:
--        - When each group member enters and leaves combat
--          (the game's official "in combat" flag — clean signal
--          for where pulls actually start and end).
--        - When the dungeon's "Enemies Remaining" bar ticks up
--          (raw timestamp + delta + total). The server pairs
--          these against UNIT_DIED entries in the bare combat
--          log to learn which mob earned the progress.
--        - Boss yells and whispers (used to mark phase changes
--          on the run sheet's boss cards).
--        - Boss-fight upcoming-cast schedules (when the game
--          publishes them — modern bosses do).
--        - The first group member to enter combat at the start
--          of every pull (the puller).
--   3. On dungeon end, the captured lists are saved to the
--      shared MPI section of saved variables. The Companion
--      reads from there next time it uploads.
-- DEPENDS ON:
--   - The shared MidnightUI_MPI saved variable (owned by
--     MidnightPI.lua — must already exist before this loads,
--     hence load order in the .toc puts this AFTER MidnightPI).
--   - Standard game built-ins. No third-party addon dependency.
-- EXPORTS:
--   _G.MidnightUI_MPICaptures (the module table — exposed for
--   debugging from the in-game console only).
-- OWNED SAVEDVARIABLES:
--   MidnightUI_MPI.captures — added each run, cleared on next.
-- EVENTS:
--   - The dungeon-start and dungeon-finish signals.
--   - Per-player combat-state flips.
--   - The dungeon progress bar updating.
--   - Boss yells and whispers.
--   - Upcoming boss-cast events (when the game publishes them).
-- GOTCHAS:
--   - All recorded times are seconds since the dungeon began
--     (offset from CHALLENGE_MODE_START), to match what the
--     server-side combat log uses. Wall-clock times don't
--     work because uploads can happen hours after the run.
--   - We capture all five things into bounded lists. If a
--     single key produces more than the cap, oldest entries
--     get dropped. Caps are deliberately generous; a 25-min
--     +14 won't come close to hitting them.
--   - WoW 12 (Midnight expansion) silently blocks every addon
--     from subscribing to COMBAT_LOG_EVENT_UNFILTERED. Details,
--     BigWigs, etc. confirm this with version-checked fallbacks
--     (Details' DF.IsAddonApocalypseWow / BigWigs's self:Retail
--     guard around boss:Death). Mob-died-at-this-moment data
--     is therefore unreachable from inside the addon. The
--     server's PHP parser pairs progress ticks against the
--     UNIT_DIED entries already present in the combat log file
--     the Companion uploads, so no information is lost — just
--     moved to where it can still run.
-- ============================================================

local M = _G.MidnightUI_MPICaptures or {}
_G.MidnightUI_MPICaptures = M

-- Cap sizes per capture bucket. A typical 25-minute key produces
-- around 30 combat-state flips, 200 progress ticks, 0-15 boss yells.
-- These caps leave plenty of headroom for a 60-min depleted +20.
local CAP_REGEN     = 600
local CAP_PROGRESS  = 800
local CAP_EMOTE     =  80
local CAP_TIMELINE  = 200
local CAP_PULLER    = 200

-- ── State ───────────────────────────────────────────────────
local runStartGameTime = nil  -- GetTime() at the moment the key began
local active           = false
local lastQuantity     = nil  -- previous progress value, used to compute deltas

-- ── Helpers ─────────────────────────────────────────────────
local function nowOffset()
    if not runStartGameTime then return nil end
    return GetTime() - runStartGameTime
end

local function pushCap(t, entry, cap)
    t[#t + 1] = entry
    while #t > cap do
        table.remove(t, 1)
    end
end

local function getCaptureBuckets()
    if not _G.MidnightUI_MPI then
        _G.MidnightUI_MPI = {}
    end
    if not _G.MidnightUI_MPI.captures then
        _G.MidnightUI_MPI.captures = {
            regen     = {},
            progress  = {},
            emote     = {},
            timeline  = {},
            puller    = {},
            startedAt = nil,
            endedAt   = nil,
        }
    end
    return _G.MidnightUI_MPI.captures
end

-- ── Reset on dungeon start ──────────────────────────────────
local function onChallengeStart()
    runStartGameTime = GetTime()
    active           = true
    lastQuantity     = nil

    if _G.MidnightUI_MPI then
        _G.MidnightUI_MPI.captures = {
            regen     = {},
            progress  = {},
            emote     = {},
            timeline  = {},
            puller    = {},
            startedAt = time and time() or nil,
            endedAt   = nil,
        }
    end
end

local function onChallengeEnd()
    active = false
    local b = _G.MidnightUI_MPI and _G.MidnightUI_MPI.captures
    if b then
        b.endedAt = time and time() or nil
    end
end

-- ── Per-player combat-state events ──────────────────────────
local function onRegenDisabled()
    if not active then return end
    local sec = nowOffset()
    if not sec or sec < 0 then return end
    local b = getCaptureBuckets()
    local who = UnitName("player")
    pushCap(b.regen, { sec = sec, who = who, kind = "in"  }, CAP_REGEN)

    -- Pull attribution: at the moment combat starts, scan the group
    -- and record whichever member already shows the in-combat flag.
    -- That's the puller. If nobody else is in combat yet, you pulled.
    local groupSize = (IsInGroup and IsInGroup() and (GetNumGroupMembers() or 0)) or 0
    local first = nil
    if groupSize > 0 then
        local prefix = (IsInRaid and IsInRaid()) and "raid" or "party"
        for i = 1, groupSize do
            local unit = prefix .. i
            if UnitExists(unit) and UnitAffectingCombat(unit) then
                first = UnitName(unit)
                break
            end
        end
    end
    if not first and UnitAffectingCombat("player") then
        first = who
    end
    if first then
        pushCap(b.puller, { sec = sec, who = first }, CAP_PULLER)
    end
end

local function onRegenEnabled()
    if not active then return end
    local sec = nowOffset()
    if not sec or sec < 0 then return end
    local b = getCaptureBuckets()
    pushCap(b.regen, { sec = sec, who = UnitName("player"), kind = "out" }, CAP_REGEN)
end

-- ── Progress-bar tick ───────────────────────────────────────
-- Records each tick of the Enemies Remaining bar as a raw (sec, delta,
-- total) entry. The server pairs these against UNIT_DIED entries in
-- the combat log file the Companion uploads.
local function onCriteriaUpdate()
    if not active then return end
    if not C_Scenario then return end

    local _, _, numCriteria = C_Scenario.GetStepInfo()
    if not numCriteria or numCriteria <= 0 then return end

    -- Find the weighted-progress criteria (the "Enemy Forces" bar).
    -- It's the only one that ticks up gradually — others are boss kills.
    for i = 1, numCriteria do
        local info = C_ScenarioInfo and C_ScenarioInfo.GetCriteriaInfo and C_ScenarioInfo.GetCriteriaInfo(i)
        if info and info.isWeightedProgress then
            local qStr = info.quantityString
            local current = qStr and tonumber(qStr:match("%d+"))
            if current and lastQuantity and current > lastQuantity and current < info.totalQuantity then
                local sec = nowOffset()
                if sec and sec >= 0 then
                    local b = getCaptureBuckets()
                    pushCap(b.progress, {
                        sec   = sec,
                        delta = current - lastQuantity,
                        total = info.totalQuantity,
                    }, CAP_PROGRESS)
                end
            end
            lastQuantity = current
            return
        end
    end
end

-- ── Boss yells and whispers (phase markers) ─────────────────
local function onBossEmote(text, sourceName)
    if not active then return end
    local sec = nowOffset()
    if not sec or sec < 0 then return end
    local b = getCaptureBuckets()
    pushCap(b.emote, { sec = sec, source = sourceName or "", text = text or "" }, CAP_EMOTE)
end

-- ── Upcoming-cast schedule (modern bosses) ──────────────────
local function onTimelineEventAdded(eventInfo)
    if not active then return end
    if not eventInfo or eventInfo.source ~= 0 then return end -- 0 = encounter-source
    local sec = nowOffset()
    if not sec or sec < 0 then return end
    local b = getCaptureBuckets()
    pushCap(b.timeline, {
        sec      = sec,
        eventID  = eventInfo.id,
        duration = eventInfo.duration,
        spellId  = eventInfo.spellID,
    }, CAP_TIMELINE)
end

-- ── Frame + event wiring ────────────────────────────────────
-- COMBAT_LOG_EVENT_UNFILTERED is intentionally never registered.
-- WoW 12 hard-blocks this addon from subscribing to the combat log
-- from any context (main chunk, PLAYER_LOGIN, deferred timer, fresh
-- frames, clean Blizzard event boundaries — all refused with
-- ADDON_ACTION_FORBIDDEN). The server-side parser falls back to
-- inferring mob-death/progress pairing from the bare combat log
-- already uploaded by the Companion.
local frame = CreateFrame("Frame")
frame:RegisterEvent("CHALLENGE_MODE_START")
frame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
frame:RegisterEvent("CHALLENGE_MODE_RESET")
frame:RegisterEvent("PLAYER_REGEN_DISABLED")
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:RegisterEvent("SCENARIO_CRITERIA_UPDATE")
frame:RegisterEvent("RAID_BOSS_EMOTE")
frame:RegisterEvent("RAID_BOSS_WHISPER")
pcall(function() frame:RegisterEvent("ENCOUNTER_TIMELINE_EVENT_ADDED") end)

frame:SetScript("OnEvent", function(_, ev, ...)
    if     ev == "CHALLENGE_MODE_START"        then onChallengeStart()
    elseif ev == "CHALLENGE_MODE_COMPLETED"    then onChallengeEnd()
    elseif ev == "CHALLENGE_MODE_RESET"        then onChallengeEnd()
    elseif ev == "PLAYER_REGEN_DISABLED"       then onRegenDisabled()
    elseif ev == "PLAYER_REGEN_ENABLED"        then onRegenEnabled()
    elseif ev == "SCENARIO_CRITERIA_UPDATE"    then onCriteriaUpdate()
    elseif ev == "RAID_BOSS_EMOTE" or ev == "RAID_BOSS_WHISPER" then
        local text, source = ...
        onBossEmote(text, source)
    elseif ev == "ENCOUNTER_TIMELINE_EVENT_ADDED" then
        onTimelineEventAdded(...)
    end
end)
