-- VDS_BlizzardEvents
-- ============================================================================
-- Subscribes module-declared Blizzard events to a single hidden frame.
-- Each subscription's `opts` can declare debounce, action shortcut,
-- requiresMainWindow, and `once`.
--
-- LESSON (VFN e8793b2f): dispatch frame MUST be live at file-load time.
-- ADDON_LOADED / PLAYER_LOGIN must be subscribable from Init.lua BEFORE
-- Modules:Boot runs. Lazy init forces a second event frame in Init (cookbook/06
-- violation). Eager init kills the egg.

VDS = VDS or {}
VDS.BlizzardEvents = VDS.BlizzardEvents or { _subscribers = {}, _frame = nil, _debounceTimers = {} }

local BE = VDS.BlizzardEvents

if _G.CreateFrame and not BE._frame then
    BE._frame = CreateFrame("Frame")
    BE._frame:SetScript("OnEvent", function(_, event, ...)
        BE:Dispatch(event, ...)
    end)
end

function BE:Subscribe(event, sub)
    -- sub: { mode = "handler"|"action", handler = fn, opts = {...} }
    if not self._frame then return end                  -- headless test path
    if not self._subscribers[event] then
        self._subscribers[event] = {}
        self._frame:RegisterEvent(event)
    end
    -- Stamp _event so _Invoke can locate the parent list for once-fire removal.
    -- Closure-subs without _event would silently leak.
    sub._event = event
    self._subscribers[event][#self._subscribers[event] + 1] = sub
end

function BE:Dispatch(event, ...)
    local subs = self._subscribers[event]
    if not subs then return end
    for _, sub in ipairs(subs) do
        self:_Fire(event, sub, ...)
    end
end

function BE:_Fire(event, sub, ...)
    local opts = sub.opts or {}
    if opts.requiresMainWindow then
        local state = VDS.Store:GetState()
        local shown = state and state.session and state.session.ui  -- exception(nullable): Blizzard events can fire before state init
                      and state.session.ui.windowShown
        if not shown then return end
    end
    if opts.filter and not opts.filter(...) then return end

    if opts.debounce and opts.debounce > 0 then
        local key = tostring(sub) .. ":" .. event
        if self._debounceTimers[key] then return end
        self._debounceTimers[key] = true
        local args = { ... }
        _G.C_Timer.After(opts.debounce, function()
            self._debounceTimers[key] = nil
            self:_Invoke(sub, table.unpack and table.unpack(args) or unpack(args))
        end)
    else
        self:_Invoke(sub, ...)
    end
end

function BE:_Invoke(sub, ...)
    local opts = sub.opts or {}
    if opts.once then
        local subs = self._subscribers[sub._event]
        if subs then
            for i, s in ipairs(subs) do
                if s == sub then table.remove(subs, i); break end
            end
        end
    end
    if sub.mode == "action" then
        VDS.Store:Dispatch({ type = opts.action, payload = { ... } })
    elseif sub.mode == "handler" then
        if sub.handler then
            -- closure-style (no module): Init.lua bootstrap subs (invariant §14).
            local args = { ... }
            local ok, err = xpcall(
                function() return sub.handler(unpack(args)) end,
                function(e) return tostring(e) .. "\n" .. _G.debugstack(2, 12, 0) end)
            if not ok then
                VDS.Log:Error("blizzevent",
                    string.format("[%s] handler: %s", tostring(sub._event), tostring(err)))
            end
        else
            local module = sub.module
            local fn = module[sub.handlerName]
            local args = { ... }
            local ok, err = xpcall(
                function() return fn(module, unpack(args)) end,
                function(e) return tostring(e) .. "\n" .. _G.debugstack(2, 12, 0) end)
            if not ok then
                VDS.Log:Error("blizzevent",
                    string.format("[%s] %s.%s: %s", tostring(sub._event),
                        tostring(module and module.name or "?"),  -- exception(false-positive): error-message composer
                        tostring(sub.handlerName), tostring(err)))
            end
        end
    end
end

function BE:WireRegistered(registry)
    for _, m in pairs(registry) do
        if type(m.blizzardEvents) == "table" then
            for event, def in pairs(m.blizzardEvents) do
                self:Subscribe(event, {
                    mode    = "handler",
                    handlerName = def.handler,
                    module  = m,
                    opts    = def,
                    _event  = event,
                })
            end
        end
    end
end
