local addonName, ns = ...

------------------------------------------------------------
-- Live availability: how many of an entry the player has free
-- and how many are already placed. All catalog access is
-- defensive: nil results render as an assumed 0, never an error.
------------------------------------------------------------

local Availability = {}
ns.Availability = Availability

local DECOR_ENTRY_TYPE = 1

-- Candidate field names for counts on the catalog entry info table.
-- The live client decides which exist (verify with /hh dump <id>, Task 5);
-- prune this list to the real fields once confirmed in-game.
local TOTAL_FIELDS = { "numOwned", "quantityOwned", "quantity" }
local AVAILABLE_FIELDS = { "numAvailable", "quantityAvailable", "numInStorage" }

local function getDecorEntryInfo(id)
    if not (C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID) then
        return nil
    end
    local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID,
        DECOR_ENTRY_TYPE, id, false)
    if ok and type(info) == "table" then return info end
    return nil
end

local function pickField(info, names)
    for _, key in ipairs(names) do
        if type(info[key]) == "number" then return info[key] end
    end
    return nil
end

local function getItemCount(itemID)
    local fn = (C_Item and C_Item.GetItemCount) or GetItemCount
    if not fn then return nil end
    -- includeBank = true: counting banked dyes as available is
    -- deliberate product behavior, not just bag contents.
    local ok, count = pcall(fn, itemID, true)
    if ok and type(count) == "number" then return count end
    return nil
end

function Availability.Get(entryType, id)
    if entryType == "DECOR" then
        local info = getDecorEntryInfo(id)
        if not info then return nil, nil end
        local total = pickField(info, TOTAL_FIELDS)
        local available = pickField(info, AVAILABLE_FIELDS)
        if available == nil then
            -- Only a total count exposed: report it as available but admit
            -- we can't split out what's already placed.
            return total, nil
        end
        local inUse
        if total then inUse = math.max(0, total - available) end
        return available, inUse
    end

    if entryType == "DYE" then
        -- Count the shade's OWN dye item, not ns.DYES[id].itemID -- that is
        -- the shared pigment, so counting it reports one pigment's total for
        -- every shade made from it and green-checks dyes the player has none
        -- of. ns.GetDyeItemID resolves and caches the per-shade item; when it
        -- can't yet, report unknown rather than a number we know is wrong.
        local itemID = ns.GetDyeItemID and ns.GetDyeItemID(id)
        if itemID then
            return getItemCount(itemID), nil
        end
        return nil, nil
    end

    return nil, nil
end

------------------------------------------------------------
-- Name / icon resolution
------------------------------------------------------------

function ns.ResolveName(entryType, id)
    if entryType == "DECOR" then
        local info = getDecorEntryInfo(id)
        return info and info.name or nil
    end
    if entryType == "DYE" then
        local dye = ns.DYES and ns.DYES[id]
        return dye and dye.name or nil
    end
    return nil
end

function ns.ResolveIcon(entryType, id)
    if entryType == "DECOR" then
        local info = getDecorEntryInfo(id)
        if not info then return nil end
        return info.iconTexture or info.icon or nil
    end
    return nil
end

------------------------------------------------------------
-- Line formatting (shared by the shopping tab and the tracker)
------------------------------------------------------------

function ns.FormatEntryLine(entry, available, inUse)
    local name = entry.name
    if not name then
        name = (entry.type == "DYE" and "Dye #" or "Decor #") .. entry.id
    end
    -- Unknown availability reads as 0: "you don't have it yet" is the
    -- useful default, and a dash next to real zeroes looked like a bug.
    local line = string.format("%d/%d %s", available or 0, entry.qty, name)
    if inUse and inUse > 0 then
        line = line .. string.format(" (%d in use)", inUse)
    end
    return line
end

------------------------------------------------------------
-- Change notification, debounced across event bursts
------------------------------------------------------------

local callbacks = {}
local pending = false

function Availability.OnChanged(cb)
    table.insert(callbacks, cb)
end

local function notify()
    pending = false
    -- pcall each subscriber: these are our own UI refreshers, and one
    -- throwing would otherwise abort the remaining subscribers and
    -- propagate out of Core's event handler, bricking the list.
    for _, cb in ipairs(callbacks) do
        pcall(cb)
    end
end

function Availability.HandleEvent(_)
    if pending then return end
    pending = true
    if C_Timer and C_Timer.After then
        C_Timer.After(0.2, notify)
    else
        notify()
    end
end
