local addonName, NS = ...

-- ============================================================================
-- VAULTSKINS: POOLED SCHEDULER
-- ============================================================================

local Stability = {}
NS.Stability = Stability

local rawAfter = C_Timer and C_Timer.After
local rawNewTimer = C_Timer and C_Timer.NewTimer
local nowFunction = GetTimePreciseSec or GetTime

local heap = {}
local keyed = {}
local entryPool = {}
-- ============================= SCHEDULER STATE ==============================
local MAX_POOLED_ENTRIES = 160
local serial = 0
local generation = 0
local activeCount = 0
local cancelledInHeap = 0
local driver

local gcEnabled = false
local gcAccumulator = 0
local gcInterval = 0.25
local gcStepSize = 192

local metrics = {
    scheduled = 0,
    executed = 0,
    cancelled = 0,
    replaced = 0,
    suppressed = 0,
    timerErrors = 0,
    themeApplyRequests = 0,
    rarityRefreshRequests = 0,
    cardBuilds = 0,
    cardBuildFailures = 0,
    animationGroupsCreated = 0,
    animationGroupsReused = 0,
    heapCompactions = 0,
    peakPending = 0,
    lastCancelReason = "none",
    lastTimerError = nil,
}

-- ================================ ENTRY POOL ================================
local function Now()
    if nowFunction then return nowFunction() end
    return 0
end

-- Pool short-lived reveal callbacks to reduce allocation spikes.
local function AcquireEntry()
    local count = #entryPool
    if count == 0 then return {} end
    local entry = entryPool[count]
    entryPool[count] = nil
    return entry
end

local function ReleaseEntry(entry)
    if not entry then return end
    entry.key = nil
    entry.group = nil
    entry.callback = nil
    entry.arg1 = nil
    entry.arg2 = nil
    entry.arg3 = nil
    entry.arg4 = nil
    entry.arg5 = nil
    entry.arg6 = nil
    entry.serial = nil
    entry.generation = nil
    entry.due = nil
    entry.cancelled = nil
    entry.handle = nil
    if #entryPool < MAX_POOLED_ENTRIES then
        entryPool[#entryPool + 1] = entry
    end
end

-- ============================== MIN-HEAP QUEUE ==============================
local function ComesBefore(first, second)
    if first.due == second.due then
        return first.serial < second.serial
    end
    return first.due < second.due
end

local function HeapPush(entry)
    local index = #heap + 1
    heap[index] = entry
    while index > 1 do
        local parent = math.floor(index / 2)
        if ComesBefore(heap[parent], entry) then break end
        heap[index] = heap[parent]
        index = parent
    end
    heap[index] = entry
end

local function HeapPop()
    local count = #heap
    if count == 0 then return nil end
    local root = heap[1]
    local last = heap[count]
    heap[count] = nil
    count = count - 1
    if count > 0 then
        local index = 1
        while true do
            local left = index * 2
            if left > count then break end
            local right = left + 1
            local child = left
            if right <= count and ComesBefore(heap[right], heap[left]) then child = right end
            if ComesBefore(last, heap[child]) then break end
            heap[index] = heap[child]
            index = child
        end
        heap[index] = last
    end
    return root
end

local function CompactHeap(force)
    if not force and (cancelledInHeap < 24 or cancelledInHeap < math.floor(#heap / 3)) then return end
    local compacted = {}
    for index = 1, #heap do
        local entry = heap[index]
        if entry and not entry.cancelled then
            compacted[#compacted + 1] = entry
        elseif entry then
            ReleaseEntry(entry)
        end
    end
    heap = {}
    for index = 1, #compacted do HeapPush(compacted[index]) end
    cancelledInHeap = 0
    metrics.heapCompactions = metrics.heapCompactions + 1
end

-- ============================= QUEUE EXECUTION ==============================
local function ExecuteEntry(entry)
    if not entry or entry.cancelled or entry.generation ~= generation then
        metrics.suppressed = metrics.suppressed + 1
        ReleaseEntry(entry)
        return
    end

    if entry.key and keyed[entry.key] == entry then keyed[entry.key] = nil end
    activeCount = math.max(0, activeCount - 1)
    metrics.executed = metrics.executed + 1

    local callback = entry.callback
    local arg1, arg2, arg3 = entry.arg1, entry.arg2, entry.arg3
    local arg4, arg5, arg6 = entry.arg4, entry.arg5, entry.arg6
    local key = entry.key or entry.serial
    ReleaseEntry(entry)

    local ok, err = pcall(callback, arg1, arg2, arg3, arg4, arg5, arg6)
    if not ok then
        metrics.timerErrors = metrics.timerErrors + 1
        metrics.lastTimerError = tostring(err)
        if NS.Skin and NS.Skin.HandleScheduledError then
            NS.Skin:HandleScheduledError(key, err)
        else
            print("|cffff5555VaultSkins:|r Protected a scheduled update failure (" .. tostring(key) .. ").")
            print("|cffaaaaaa" .. tostring(err) .. "|r")
        end
    end
end

local function ProcessQueue(elapsed)
    local now = Now()
    while true do
        local entry = heap[1]
        if not entry or entry.due > now then break end
        entry = HeapPop()
        if entry.cancelled then
            cancelledInHeap = math.max(0, cancelledInHeap - 1)
            ReleaseEntry(entry)
        else
            ExecuteEntry(entry)
        end
    end

    if gcEnabled and type(collectgarbage) == "function" then
        gcAccumulator = gcAccumulator + (tonumber(elapsed) or 0)
        if gcAccumulator >= gcInterval then
            gcAccumulator = 0
            collectgarbage("step", gcStepSize)
        end
    end

    CompactHeap()
    if driver and activeCount == 0 and not gcEnabled then
        driver:SetScript("OnUpdate", nil)
    end
end

-- ========================= DRIVER AND CANCELLATION ==========================
local function EnsureDriver()
    if driver then
        if not driver:GetScript("OnUpdate") then driver:SetScript("OnUpdate", ProcessQueue) end
        return true
    end
    if type(CreateFrame) ~= "function" then return false end
    driver = CreateFrame("Frame")
    driver:SetScript("OnUpdate", ProcessQueue)
    return true
end

local function CancelEntry(entry)
    if not entry or entry.cancelled then return false end
    entry.cancelled = true
    if entry.key and keyed[entry.key] == entry then keyed[entry.key] = nil end
    activeCount = math.max(0, activeCount - 1)
    cancelledInHeap = cancelledInHeap + 1
    metrics.cancelled = metrics.cancelled + 1
    if entry.handle and entry.handle.Cancel then pcall(entry.handle.Cancel, entry.handle) end
    return true
end

-- =========================== PUBLIC SCHEDULER API ===========================
function Stability:Record(metric, amount)
    if metrics[metric] == nil then metrics[metric] = 0 end
    metrics[metric] = (tonumber(metrics[metric]) or 0) + (tonumber(amount) or 1)
end

function Stability:ScheduleCall(key, delay, callback, arg1, arg2, arg3, arg4, arg5, arg6, group)
    if type(callback) ~= "function" then return nil end
    delay = math.max(0, tonumber(delay) or 0)

    if key and keyed[key] then
        if CancelEntry(keyed[key]) then metrics.replaced = metrics.replaced + 1 end
    end

    serial = serial + 1
    local entry = AcquireEntry()
    entry.key = key
    entry.group = group
    entry.callback = callback
    entry.arg1 = arg1
    entry.arg2 = arg2
    entry.arg3 = arg3
    entry.arg4 = arg4
    entry.arg5 = arg5
    entry.arg6 = arg6
    entry.serial = serial
    entry.generation = generation
    entry.due = Now() + delay
    entry.cancelled = false

    metrics.scheduled = metrics.scheduled + 1
    activeCount = activeCount + 1
    if activeCount > metrics.peakPending then metrics.peakPending = activeCount end
    if key then keyed[key] = entry end

    if EnsureDriver() then
        HeapPush(entry)
    else
        local function FallbackRun()
            if entry.cancelled or entry.generation ~= generation then
                ReleaseEntry(entry)
                return
            end
            ExecuteEntry(entry)
        end
        if rawNewTimer then
            entry.handle = rawNewTimer(delay, FallbackRun)
        elseif rawAfter then
            rawAfter(delay, FallbackRun)
        else
            FallbackRun()
        end
    end
    return key or serial
end

function Stability:Schedule(key, delay, callback)
    return self:ScheduleCall(key, delay, callback)
end

function Stability:Cancel(key)
    local entry = key and keyed[key]
    return CancelEntry(entry)
end

function Stability:CancelGroup(group, reason)
    if group == nil then return 0 end
    local count = 0
    for index = 1, #heap do
        local entry = heap[index]
        if entry and not entry.cancelled and entry.group == group then
            if CancelEntry(entry) then count = count + 1 end
        end
    end
    if count > 0 then metrics.lastCancelReason = tostring(reason or group) end
    CompactHeap(true)
    return count
end

function Stability:CancelAll(reason)
    generation = generation + 1
    local count = 0
    for index = 1, #heap do
        local entry = heap[index]
        if entry and not entry.cancelled then
            entry.cancelled = true
            count = count + 1
        end
        ReleaseEntry(entry)
        heap[index] = nil
    end
    for key in pairs(keyed) do keyed[key] = nil end
    activeCount = 0
    cancelledInHeap = 0
    metrics.cancelled = metrics.cancelled + count
    metrics.lastCancelReason = tostring(reason or "unspecified")
    if driver and not gcEnabled then driver:SetScript("OnUpdate", nil) end
    return count
end

function Stability:IsPending(key)
    local entry = key and keyed[key]
    return entry ~= nil and not entry.cancelled
end

function Stability:GetPendingCount()
    return activeCount
end

-- ====================== DIAGNOSTICS AND INCREMENTAL GC ======================
function Stability:SetIncrementalGC(enabled, interval, stepSize)
    gcEnabled = enabled and true or false
    gcAccumulator = 0
    gcInterval = math.max(0.10, tonumber(interval) or 0.25)
    gcStepSize = math.max(64, tonumber(stepSize) or 192)
    if gcEnabled then
        EnsureDriver()
    elseif driver and activeCount == 0 then
        driver:SetScript("OnUpdate", nil)
    end
end

function Stability:GetDiagnostics()
    local copy = {}
    for key, value in pairs(metrics) do copy[key] = value end
    copy.pending = activeCount
    copy.heapSize = #heap
    copy.poolSize = #entryPool
    copy.generation = generation
    copy.incrementalGC = gcEnabled
    return copy
end

function Stability:ResetMetrics()
    local pending = activeCount
    for key, value in pairs(metrics) do
        if type(value) == "number" then metrics[key] = 0 end
    end
    metrics.lastCancelReason = "metrics reset"
    metrics.lastTimerError = nil
    return pending
end
