-- VDS_Modules
-- ============================================================================
-- Module registry with dependency-DAG boot. Modules:Declare(spec) registers a
-- module; Modules:Boot() walks the DAG in topological order and runs
-- onInitialize then onEnable for each.
--
-- API NAMESPACE OWNERSHIP (cookbook 06, ADR-023): each module declares
--   ownsBlizzardNamespaces = {"C_Housing", ...}
-- ValidateOwnership() asserts no namespace is claimed by two modules.
-- Sweep enforces no other file reads from claimed namespaces.

VDS = VDS or {}
VDS.Modules = VDS.Modules or { _registry = {}, _booted = false }

local M = VDS.Modules

function M:Declare(spec)
    if not spec or not spec.name then error("Modules:Declare: missing name", 2) end
    self._registry[spec.name] = spec
    -- Hoist onto VDS namespace so callers don't dig through Modules._registry.
    -- Matches Controller_* bare-global pattern.
    VDS[spec.name] = spec
    if self._booted and spec.onInitialize then spec:onInitialize() end
end

function M:ValidateOwnership()
    local owner = {}
    for name, m in pairs(self._registry) do
        for _, ns in ipairs(m.ownsBlizzardNamespaces or {}) do
            if owner[ns] then
                error(("Modules: Blizzard namespace %q claimed by both %q and %q"):format(
                    ns, owner[ns], name), 2)
            end
            owner[ns] = name
        end
    end
end

local function topoSort(registry)
    local order, visited, visiting = {}, {}, {}
    local function visit(name)
        if visited[name] then return end
        if visiting[name] then error("Modules: cycle at " .. name, 3) end
        visiting[name] = true
        local m = registry[name]
        if not m then error("Modules: unknown dep " .. name, 3) end
        for _, dep in ipairs(m.dependencies or {}) do visit(dep) end
        visiting[name] = nil
        visited[name] = true
        order[#order + 1] = m
    end
    for name in pairs(registry) do visit(name) end
    return order
end

function M:Boot()
    if self._booted then return end
    self:ValidateOwnership()                              -- boot-disjoint check
    local order = topoSort(self._registry)
    for _, m in ipairs(order) do
        if m.onInitialize then
            local ok, err = pcall(m.onInitialize, m)
            -- §9: swallowed init leaves state slot unseeded; addon dispatches against missing shape.
            if not ok then VDS.Log:Error("module", m.name .. ".onInitialize: " .. tostring(err)) end
        end
    end
    -- Wire BEFORE onEnable so onEnable can dispatch against live subscriptions.
    VDS.BlizzardEvents:WireRegistered(self._registry)
    for _, m in ipairs(order) do
        if m.onEnable then
            local ok, err = pcall(m.onEnable, m)
            if not ok then VDS.Log:Error("module", m.name .. ".onEnable: " .. tostring(err)) end
        end
    end
    self._booted = true
end

function M:Shutdown()
    local order = topoSort(self._registry)
    for i = #order, 1, -1 do
        local m = order[i]
        if m.onShutdown then pcall(m.onShutdown, m) end  -- exception(optional): onShutdown is an opt-in module lifecycle hook
    end
end
