--[[ Core/Catalog.lua — catalog entry info + ownership (spike-bound names).
     REAL functions (M0): GetCatalogEntryInfoByRecordID, GetCatalogEntryInfo,
     GetDecorTotalOwnedCount, GetDecorMaxOwnedCount, CreateCatalogSearcher.

     Since the server computes numMissing per player in blueprint contents,
     Catalog is an ENRICHMENT layer (icons, categories, ownership for baked
     manifests when no live payload exists) — not the score's backbone.

     VERIFY (M2 in-game): exact field names in the entry-info struct. We probe
     defensively: any numeric field matching "own" is treated as owned count. ]]--

local _, ns = ...
local Catalog = {}
ns.Catalog = Catalog

local CC = C_HousingCatalog
local infoCache = {}   -- [recordID] = info table | false (=lookup failed)

-- VERIFIED IN GAME 2026-08-14: the call is
--   GetCatalogEntryInfoByRecordID(entryType, recordID)
-- Two arguments, not one. We were calling it with one inside a pcall, so it
-- has been failing silently since day one - which is why ownership was always
-- "unknown" and every score had to come from the server.
--
-- Returned fields that matter: quantity (owned), itemID (the real WoW item
-- behind the decor), placementCost, isAllowedIndoors/Outdoors, categoryIDs.
-- There is NO vendor or source field anywhere in C_HousingCatalog.
local ENTRY_TYPES = { 1, 3, 0, 2, 4, 5 }   -- 1 = decor, the common case first

-- The catalog is not populated the instant the addon loads. MEASURED
-- 2026-08-16: /bb diff on bb-0031 straight after /reload reported three decor
-- records as unowned that the server said were owned; the same command a
-- minute later agreed on all of them. Nothing about the maths changed - the
-- first read simply happened before the client had the data.
--
-- The damage was the cache: a cold "not found" was stored like a fact and
-- stuck for the session, so an unlucky first open left every score too low
-- until something happened to clear it. So failures are only remembered once
-- the catalog has proven it can answer at all, and the moment it wakes up
-- everything computed from the cold reads is thrown away.
local catalogWarm = false

function Catalog.InfoByRecord(recordID)
  if not recordID then return nil end
  local hit = infoCache[recordID]
  if hit ~= nil then return hit or nil end
  local fn = CC and CC.GetCatalogEntryInfoByRecordID
  if not fn then return nil end
  for _, entryType in ipairs(ENTRY_TYPES) do
    local ok, info = pcall(fn, entryType, recordID)
    if ok and type(info) == "table" then
      infoCache[recordID] = info
      if not catalogWarm then
        -- First real answer of the session: anything scored before this was
        -- guessed against an empty catalog and has to be recomputed.
        catalogWarm = true
        wipe(infoCache)
        infoCache[recordID] = info
        ns.bus:Fire("OWNERSHIP_CHANGED")
      end
      return info
    end
  end
  -- Only trust a miss once we know the catalog is answering. Dye genuinely
  -- has no entry, so it lands here every time - and that is correct.
  if catalogWarm then infoCache[recordID] = false end
  return nil
end

--- Owned count for a record. Returns number or nil (=unknown).
---
--- VERIFIED IN GAME 2026-08-16 (/bb probe 274464): GetCatalogEntryInfoByRecordID
--- returns nothing at all for dye, on every entryType - but GetDecorTotalOwnedCount
--- answers 325. Dye was never invisible to the client; we were only ever asking
--- the one API that does not know about it. That single missing call is why
--- every build's score had to be confirmed with Verify by hand.
--- MEASURED 2026-08-16 (/bb probe written to SavedVariables, /bb diff against
--- the server on bb-0022):
---   Silvermoon Round Platform  quantity=0  remainingRedeemable=4  server: owned
---   Stonework Fountain         quantity=0  remainingRedeemable=0  server: not owned
--- Housing decor is an entitlement you redeem copies from, so `quantity` alone
--- is only what the player is holding right now - it read as "owns nothing" for
--- everything still sitting unclaimed. The count that matches the server is
--- what you hold plus what you may still claim.
---
--- NOT GetDecorTotalOwnedCount: it answered 325 for all three records probed,
--- including a dye the player owns none of. It does not key on recordID.
function Catalog.OwnedByRecord(recordID)
  if not recordID then return nil end
  local info = Catalog.InfoByRecord(recordID)
  if not info then return nil end          -- dye has no catalog entry at all
  local held = tonumber(info.quantity)
  local claimable = tonumber(info.remainingRedeemable)
  if held or claimable then
    return (held or 0) + (claimable or 0)
  end
  -- older field names, kept as a fallback in case the struct shifts again
  for _, key in ipairs({ "numOwned", "ownedCount", "quantityOwned", "totalOwned" }) do
    if type(info[key]) == "number" then return info[key] end
  end
  return nil
end

--- The real WoW item behind a decor piece. Gives us its icon and tooltip,
--- which is the closest thing to "where does this come from" that the API
--- offers - C_HousingCatalog has no vendor or source data at all.
function Catalog.ItemIDByRecord(recordID)
  local info = Catalog.InfoByRecord(recordID)
  return info and info.itemID or nil
end

--- Icon for a record: the catalog struct has no icon field, but the item does.
function Catalog.IconByRecord(recordID)
  local info = Catalog.InfoByRecord(recordID)
  if not info then return nil end
  if info.icon or info.iconFileID or info.iconTexture then
    return info.icon or info.iconFileID or info.iconTexture
  end
  if info.itemID and C_Item and C_Item.GetItemIconByID then
    local ok, icon = pcall(C_Item.GetItemIconByID, info.itemID)
    if ok then return icon end
  end
  return nil
end

--- Budget cost of placing one of these (from placementCost).
function Catalog.PlacementCostByRecord(recordID)
  local info = Catalog.InfoByRecord(recordID)
  return info and info.placementCost or nil
end

function Catalog.Invalidate()
  wipe(infoCache)
  ns.bus:Fire("OWNERSHIP_CHANGED")
end

-- Ownership can change when decor is bought/redeemed; the exact event name is
-- VERIFY — register candidates, harmless if they don't exist.
for _, ev in ipairs({ "HOUSING_CATALOG_UPDATED", "HOUSING_STORAGE_UPDATED" }) do
  ns.events:Register(ev, function() Catalog.Invalidate() end)
end
