-- ============================================================================
-- Core/Perf.lua — lightweight slow-task profiler
-- ============================================================================
-- Purpose: stop guessing where a hitch comes from. ETB does its deferred work
-- through two dispatchers — the startup scheduler (RunWhenStartupIdle) and the
-- work pump (QueueWork). This times each task they run and records anything that
-- overruns a frame budget, so a stutter can be attributed to a NAMED task with a
-- duration instead of inferred from code reading.
--
-- Cost when nothing is slow: one debugprofilestop() pair per measured task plus a
-- couple of table writes. Negligible. The hooks live in Core/Init.lua and call
-- ETB.Perf:Record(label, ms); this file just owns the storage + reporting.
--
-- Commands (wired in Core/Commands.lua):
--   /etb perf          show worst offenders + recent slow tasks
--   /etb perf reset    clear the collected data
--   /etb perf off|on   toggle collection
-- ============================================================================

local ADDON_NAME, NS = ...
local ETB = NS.ETB
ETB.Perf = ETB.Perf or {}
local Perf = ETB.Perf

Perf.SLOW_MS  = Perf.SLOW_MS or 8     -- a task over ~half a 60fps frame is notable
Perf.RING     = Perf.RING or 40       -- keep the last N slow tasks
Perf.enabled  = (Perf.enabled ~= false)
Perf.slow     = Perf.slow or {}       -- ring buffer: { label, ms, at }
Perf._ring    = Perf._ring or 0
Perf.tally    = Perf.tally or {}      -- label -> { count, total, max }

function Perf:Record(label, ms)
    if not self.enabled or not label or not ms then return end
    local t = self.tally[label]
    if not t then t = { count = 0, total = 0, max = 0 }; self.tally[label] = t end
    t.count = t.count + 1
    t.total = t.total + ms
    if ms > t.max then t.max = ms end
    if ms >= self.SLOW_MS then
        self._ring = (self._ring % self.RING) + 1
        self.slow[self._ring] = { label = label, ms = ms, at = (GetTime and GetTime()) or 0 }
    end
end

function Perf:Reset()
    self.slow = {}; self._ring = 0; self.tally = {}
end

-- Render a report into the chat frame.
function Perf:Print()
    if not ETB.Print then return end
    if not self.enabled then
        ETB:Print("|cffffd100ETB perf:|r collection is OFF. |cffffd100/etb perf on|r to enable.")
        return
    end

    -- Worst offenders by single-run max.
    local rows = {}
    for label, t in pairs(self.tally) do
        rows[#rows + 1] = { label = label, count = t.count, total = t.total, max = t.max }
    end
    table.sort(rows, function(a, b) return a.max > b.max end)

    ETB:Print("|cff00ffff=== ETB Perf — worst tasks (by single-run ms) ===|r")
    if #rows == 0 then
        ETB:Print("No tasks recorded yet. Reproduce the hitch, then run this again.")
    else
        for i = 1, math.min(8, #rows) do
            local r = rows[i]
            local color = (r.max >= self.SLOW_MS) and "ffff6666" or "ff66dd66"
            ETB:Print(string.format("  |c%s%6.1f ms|r max  (%d runs, %.0f ms total)  |cffffd100%s|r",
                color, r.max, r.count, r.total, r.label))
        end
    end

    -- Recent slow events in chronological-ish order (ring buffer, newest-ish last).
    local recent = {}
    for _, e in pairs(self.slow) do recent[#recent + 1] = e end
    table.sort(recent, function(a, b) return (a.at or 0) < (b.at or 0) end)
    if #recent > 0 then
        ETB:Print(string.format("|cff00ffffRecent tasks over %d ms:|r", self.SLOW_MS))
        local now = (GetTime and GetTime()) or 0
        for i = math.max(1, #recent - 7), #recent do
            local e = recent[i]
            ETB:Print(string.format("  |cffff6666%6.1f ms|r  %5.1fs ago  |cffffd100%s|r",
                e.ms, now - (e.at or now), e.label))
        end
    end
    -- Ground truth from Blizzard's built-in profiler (C_AddOnProfiler, added in
    -- 11.0.7 / MoP Classic 5.5.4; always-on, no CVar needed). The self-instrumented
    -- tasks above only see code we explicitly wrapped — this measures EVERY frame
    -- ETB spent CPU on, including timers, events and OnUpdate handlers we never
    -- instrumented (the exact blind spot that hid the CommunitySets resolve freeze).
    -- PeakTime is the single worst frame since reload; CountTimeOver* are how many
    -- frames crossed each threshold. This is the authoritative "did we ever stutter".
    if C_AddOnProfiler and C_AddOnProfiler.GetAddOnMetric and Enum and Enum.AddOnProfilerMetric then
        local M = Enum.AddOnProfilerMetric
        local function metric(m)
            local ok, v = pcall(C_AddOnProfiler.GetAddOnMetric, "EnhancedTransmogBrowser", m)
            return (ok and tonumber(v)) or 0
        end
        local peak   = metric(M.PeakTime)
        local recent = metric(M.RecentAverageTime)
        local over5, over10, over50, over100 =
            metric(M.CountTimeOver5Ms), metric(M.CountTimeOver10Ms),
            metric(M.CountTimeOver50Ms), metric(M.CountTimeOver100Ms)
        ETB:Print("|cff00ffffGround truth (Blizzard profiler — every frame, incl. uninstrumented):|r")
        local pcolor = (peak >= self.SLOW_MS) and "ffff6666" or "ff66dd66"
        ETB:Print(string.format(
            "  peak frame |c%s%.1f ms|r since reload  |  recent avg %.2f ms  |  frames over 5/10/50/100ms: %d/%d/%d/%d",
            pcolor, peak, recent, over5, over10, over50, over100))
    end

    ETB:Print(string.format("Threshold: %d ms. |cffffd100/etb perf reset|r to clear.", self.SLOW_MS))
end
