-- VDS_DyeOwnershipObserver
-- ============================================================================
-- Boundary observer for the dye ownership domain. Watches BAG_UPDATE +
-- PLAYER_ENTERING_WORLD; scans C_DyeColor; dispatches OWNERSHIP_TICK and
-- DYEABLE_ITEMS_UPDATE actions. The reducer + selectors consume from
-- session.dyes.ownership + account.dyeableItems -- no other module reads
-- C_DyeColor directly.
--
-- OWNS:        C_DyeColor.*, BAG_UPDATE, PLAYER_ENTERING_WORLD
-- FORBIDDEN:   any other file reading C_DyeColor.* directly.

VDS = VDS or {}

local A = VDS.Constants.ACTIONS
local C = VDS.Constants

VDS.Modules:Declare({
    name = "DyeOwnershipObserver",
    dependencies = {},

    ownsBlizzardNamespaces = { "C_DyeColor" },

    blizzardEvents = {
        BAG_UPDATE = {
            handler  = "OnBagUpdate",
            debounce = C.OWNERSHIP_DEBOUNCE,   -- 0.5s (donor cadence)
        },
        PLAYER_ENTERING_WORLD = {
            handler  = "OnPlayerEnteringWorld",
        },
    },

    onEnable = function(self)
        self:Scan()  -- populate ownership before any UI opens
    end,

    OnBagUpdate = function(self)
        self:Scan()
    end,

    OnPlayerEnteringWorld = function(self)
        self:Scan()
    end,

    -- Dedupes against last-dispatched sig (HDGR HousingObserver pattern).
    Scan = function(self)
        local categories = _G.C_DyeColor.GetAllDyeColorCategories() or {}
        local ownedCount, totalCount = 0, 0
        local perDye  = {}
        local allDyes = {}
        -- infos: per-dye static metadata; selectors filter on this, never call C_DyeColor (invariant §1).
        local infos = {}
        -- itemIDToDye: lets selectors resolve palette itemIDs to dyeIDs without calling C_DyeColor.
        local itemIDToDye = {}
        -- categoryNames: catID -> name; published so Components_Custom can resolve without calling C_DyeColor.
        local categoryNames = {}
        for _, catID in ipairs(categories) do
            local catInfo = _G.C_DyeColor.GetDyeColorCategoryInfo(catID)
            if catInfo then categoryNames[catID] = catInfo.name end
        end
        local SD = VDS.StaticData
        for _, catID in ipairs(categories) do
            local dyeIDs = _G.C_DyeColor.GetDyeColorsInCategory(catID) or {}
            for _, dyeID in ipairs(dyeIDs) do
                local info = _G.C_DyeColor.GetDyeColorInfo(dyeID)
                if info then
                    totalCount = totalCount + 1
                    allDyes[#allDyes + 1] = dyeID
                    if info.numOwned and info.numOwned > 0 then
                        ownedCount = ownedCount + 1
                        perDye[dyeID] = info.numOwned
                    end
                    local cs = info.swatchColorStart   -- gradient start
                    local ce = info.swatchColorEnd     -- gradient end (slotRow / recentDyeStrip)
                    local colorGroup = nil
                    if cs then  -- exception(boundary): swatchColorStart can be nil on partial C_DyeColor info
                        colorGroup = SD.Classify(info.name, cs.r, cs.g, cs.b)
                    end
                    infos[dyeID] = {
                        name        = info.name,
                        nameLower   = info.name and info.name:lower() or nil,
                        colorGroup  = colorGroup,
                        itemID      = info.itemID,
                        catID       = catID,
                        -- swatchRGB: single-point convenience for legacy callers.
                        swatchRGB   = cs and { r = cs.r, g = cs.g, b = cs.b } or nil,
                        -- Gradient endpoints for slotRow / recentDyeStrip painting.
                        swatchStart = cs and { r = cs.r, g = cs.g, b = cs.b } or nil,
                        swatchEnd   = ce and { r = ce.r, g = ce.g, b = ce.b } or nil,
                    }
                    if info.itemID then itemIDToDye[info.itemID] = dyeID end
                end
            end
        end

        -- DYE_DATA_INIT is once-per-session; infos are static so only OWNERSHIP_TICK fires on subsequent scans.
        if not self._catalogPublished and #allDyes > 0 then
            self._catalogPublished = true
            VDS.Store:Dispatch({
                type    = A.DYE_DATA_INIT,
                payload = { categories = categories, dyes = allDyes,
                            infos = infos, itemIDToDye = itemIDToDye,
                            categoryNames = categoryNames },
            })
            VDS.Log:Info("dyes", ("DYE_DATA_INIT dispatched (%d dyes, %d categories)")
                :format(#allDyes, #categories))
        end

        local sig = ownedCount .. "/" .. totalCount
        if sig == self._lastSig then return end
        self._lastSig = sig

        VDS.Store:Dispatch({
            type = A.OWNERSHIP_TICK,
            payload = { ownedCount = ownedCount, totalCount = totalCount, perDye = perDye },
        })
    end,

    -- Resolver surface: reads session.dyes.catalog, never calls C_DyeColor (invariant §9).
    -- Pre-catalog returns nil; widgets render placeholder.
    ResolveInfo = function(_self, dyeColorID)
        if not dyeColorID then return nil end
        local catalog = VDS.Store.state.session.dyes.catalog
        return catalog and catalog.infos and catalog.infos[dyeColorID]  -- exception(nullable): pre-catalog returns nil; widgets render placeholder
    end,

    ResolveCategoryName = function(_self, catID)
        if not catID then return nil end
        local catalog = VDS.Store.state.session.dyes.catalog
        return catalog and catalog.categoryNames and catalog.categoryNames[catID]  -- exception(nullable): pre-catalog returns nil; widgets render placeholder
    end,
})
