-- VDS.BagObserver
-- ============================================================================
-- Ported from HDGR.BagObserver (Modules/HDGR_BagObserver.lua). Thin
-- pull-on-demand wrapper around C_Item.GetItemCount + a tick-bump on
-- BAG_UPDATE_DELAYED. Selectors include "session.bag.tick" in `reads` so
-- they invalidate when bag changes; they call BagObserver:GetTotal(id) for
-- the actual count. GetItemCount is O(1) in Blizzard's client cache --
-- HDGR's been shipping this for years without perf issues.
--
-- OWNS:       C_Item.GetItemCount, BAG_UPDATE_DELAYED, BANKFRAME_OPENED
-- FORBIDDEN:  any other file calling C_Item.GetItemCount directly.
--
-- NOTE: methods MUST live inside the Modules:Declare spec -- the registry
-- assigns `VDS[spec.name] = spec`, which overwrites any pre-Declare table
-- assignment. Same pattern as DecorInfoResolver and ItemNameResolver.

VDS = VDS or {}

local A = VDS.Constants.ACTIONS

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

    -- Single namespace declaration (§13). Other modules must NOT call
    -- C_Item.GetItemCount directly; consume :GetTotal instead.
    ownsBlizzardNamespaces = { "C_Item.GetItemCount" },

    blizzardEvents = {
        BAG_UPDATE_DELAYED                    = { handler = "OnBagUpdate" },
        BANKFRAME_OPENED                      = { handler = "OnBagUpdate" },
        PLAYERBANKSLOTS_CHANGED               = { handler = "OnBagUpdate", debounce = 0.3 },
        PLAYER_ACCOUNT_BANK_TAB_SLOTS_CHANGED = { handler = "OnBagUpdate", debounce = VDS.Constants.OWNERSHIP_DEBOUNCE },
    },

    -- Canonical per-item total: bag + bank + reagent bank + warband.
    -- GetItemCount returns 0 (not nil) for missing items; no `or 0` needed.
    GetTotal = function(_self, itemID)
        if not itemID then return 0 end
        return _G.C_Item.GetItemCount(itemID, true, false, true, true)  -- boundary
    end,

    -- Bump the bag tick + dispatch. Selectors reading session.bag.tick
    -- invalidate; the next render pulls fresh counts via GetTotal per item.
    Scan = function(self)
        VDS.Store:Dispatch({ type = A.BAG_TICK })
    end,

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

    -- Walk every itemID the warehouse needs: dye items + all herb items across
    -- every pigment recipe. One C_Item.GetItemCount call per unique itemID
    -- (bags + bank + reagent bank + warband). Emits a FRESH perItem table per
    -- scan; the reducer replaces session.stock.perItem wholesale + bumps tick.
    -- Silently skips the herb loop if VDS_DyeCraftingDB isn't loaded yet.
    ScanWarehouseStock = function(self)
        local catalog = VDS.Store.state.session.dyes.catalog
        if not catalog or not catalog.infos then return end

        local perItem = {}

        -- Dye items
        for _, info in pairs(catalog.infos) do
            local id = info.itemID
            if id and perItem[id] == nil then
                perItem[id] = _G.C_Item.GetItemCount(id, true, false, true, true)  -- boundary: WoW API
            end
        end

        -- Herb items (all unique herbs across all pigments). Static-data
        -- access goes through the SD facade per §1 boundary discipline.
        local pigments = VDS.StaticData.GetPigments()
        if pigments then
            for _, pigment in pairs(pigments) do
                for _, herbID in ipairs(pigment.herbs) do
                    if perItem[herbID] == nil then
                        perItem[herbID] = _G.C_Item.GetItemCount(herbID, true, false, true, true)  -- boundary
                    end
                end
            end
        end

        local sig = self:_buildSig(perItem)
        if sig == self._lastStockSig then return end
        self._lastStockSig = sig

        VDS.Store:Dispatch({
            type    = VDS.Constants.ACTIONS.STOCK_TICK,
            payload = { perItem = perItem },
        })
    end,

    -- Build a sorted "id:count|id:count|..." signature for deduplication.
    -- Cheap for ~100 items; collision-resistant at this scale for v1.
    _buildSig = function(_self, m)
        local parts = {}
        for k, v in pairs(m) do parts[#parts + 1] = k .. ":" .. v end
        table.sort(parts)
        return table.concat(parts, "|")
    end,

    onEnable = function(self)
        -- First scan so bag-dependent selectors have a valid tick before
        -- the first window open. GetTotal calls return 0 until bags
        -- populate; subsequent BAG_UPDATE_DELAYED ticks invalidate.
        self:Scan()
    end,
})
