--[[
============================================================================
ETB Core/CollectionAPI.lua  (added in v15.4.0)
============================================================================

Centralized collection-state queries. Owns:
  - itemID -> appearanceID/sourceID resolution (with link-fallback)
  - "Is this appearance collected?" with the 3-fallback chain
  - "Is this collected via THIS itemID, or known from a different source?"
  - "Can the player learn this item?" with reason codes
  - An LRU cache so repeated mouseovers don't redo the chain

Why this exists:
  Three modules previously each re-implemented these queries with subtle
  differences:
    - Modules/CollectionTooltip.lua    (now dead code, disabled by TooltipEnhancer)
    - Modules/TooltipEnhancer.lua      (the active tooltip path)
    - Modules/BossLootTooltip/Render.lua  (boss-mouseover tooltip)
  v15.4.0 introduces this single source of truth; v15.5.0 will migrate the
  callers to use it. No call-site changes in v15.4.0.

Public API:
  ETB.CollectionAPI:GetState(itemID)        -> state, nil  (see states below)
  ETB.CollectionAPI:CanLearn(itemID)        -> ok, reason  (see reasons below)
  ETB.CollectionAPI:GetAppearance(itemID)   -> appearanceID, sourceID
  ETB.CollectionAPI:Invalidate(itemID)
  ETB.CollectionAPI:ClearCache()
  ETB.CollectionAPI:GetStats()              -> cache stats table

States returned by GetState:
  "COLLECTED"      — player owns this exact source (this itemID specifically)
  "KNOWN_OTHER"    — appearance is collected via a DIFFERENT itemID
  "NOT_COLLECTED"  — appearance exists but player doesn't have any of its sources
  "NO_APPEARANCE"  — item has no transmog appearance (consumable, trinket, etc.)
  nil              — item info not loaded yet by the WoW client

Reasons returned by CanLearn when ok=false:
  "NOT_EQUIPPABLE"     — empty equipLoc or INVTYPE_NON_EQUIP, or excluded slot
                         (trinket/ring/neck don't have visible appearances)
  "WRONG_ITEM_CLASS"   — classID isn't weapon (2) or armor (4)
  "WRONG_ARMOR_TYPE"   — armor subclass doesn't match player's wearable types
                         (e.g. cloth wearer looking at leather)
  "WRONG_WEAPON_TYPE"  — weapon subclass not in player's proficiency
                         (e.g. mage looking at axe)
  When ok=true, reason is always nil.

Edge case: if GetItemInfoInstant returns nothing (item not loaded), we
return nil, nil — caller should register for GET_ITEM_INFO_RECEIVED and
retry. This matches the behavior of BlizzardData.

Cache invalidation events fired elsewhere:
  - TRANSMOG_COLLECTION_SOURCE_ADDED  -> ClearCache (collection grew)
  - GET_ITEM_INFO_RECEIVED            -> Invalidate(itemID) (data arrived)
  These should be wired in v15.5.0 when callers are migrated.
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB
if not ETB then return end

ETB.CollectionAPI = ETB.CollectionAPI or {}
local CAPI = ETB.CollectionAPI

-- ============================================================================
-- CONSTANTS
-- ============================================================================

-- Transmog applies only to weapons (classID 2) and armor (classID 4)
local VALID_ITEM_CLASSES = { [2] = true, [4] = true }

-- Equip slots that don't have visible appearances even if classID is valid.
-- INVTYPE_TRINKET / FINGER / NECK have classID=4 in some clients but never
-- have transmog appearances in MoP 5.4.8.
local EXCLUDED_EQUIP_LOCS = {
    INVTYPE_TRINKET    = true,
    INVTYPE_FINGER     = true,
    INVTYPE_NECK       = true,
    INVTYPE_BAG        = true,
    INVTYPE_QUIVER     = true,
    INVTYPE_AMMO       = true,
    INVTYPE_HOLDABLE   = false, -- off-hand visible appearances DO exist
    INVTYPE_RELIC      = true,  -- legacy slot, no transmog
    INVTYPE_NON_EQUIP  = true,
    [""]               = true,  -- empty equipLoc
}

-- Armor subclass -> armor-type token, matching BlizzardData's API
local ARMOR_SUBCLASS_MAP = {
    [1] = "CLOTH",
    [2] = "LEATHER",
    [3] = "MAIL",
    [4] = "PLATE",
}

-- Expose for other modules to reuse (BLT/Render currently has its own copies;
-- v15.5.0 migration will remove the duplicates).
CAPI.VALID_ITEM_CLASSES   = VALID_ITEM_CLASSES
CAPI.EXCLUDED_EQUIP_LOCS  = EXCLUDED_EQUIP_LOCS
CAPI.ARMOR_SUBCLASS_MAP   = ARMOR_SUBCLASS_MAP

-- ============================================================================
-- CACHE
-- ============================================================================
-- Lazy-create the cache on first use, since this file loads before
-- LRUCache.lua is guaranteed to have created ETB:CreateLRUCache.

local cache = nil
local function getCache()
    if cache then return cache end
    if ETB.CreateLRUCache then
        cache = ETB:CreateLRUCache(500, "CollectionAPI")
    end
    return cache
end

-- A cache entry is a small table:
--   { appearanceID, sourceID, state, canLearnOk, canLearnReason }
-- All five fields are populated together on first lookup. Re-queries within
-- the cache window get the same result with no API calls.

-- ============================================================================
-- INTERNAL: resolve appearanceID / sourceID via two paths
-- ============================================================================

local function _resolveAppearance(itemID)
    if not itemID or not C_TransmogCollection or not C_TransmogCollection.GetItemInfo then
        return nil, nil
    end
    -- Primary: numeric itemID lookup
    local ok, appearanceID, sourceID = pcall(C_TransmogCollection.GetItemInfo, itemID)
    if ok and appearanceID then return appearanceID, sourceID end
    -- Fallback: full item link (some heroic/transmog-only items only resolve via link)
    if GetItemInfo then
        local _, itemLink = GetItemInfo(itemID)
        if itemLink then
            ok, appearanceID, sourceID = pcall(C_TransmogCollection.GetItemInfo, itemLink)
            if ok and appearanceID then return appearanceID, sourceID end
        end
    end
    return nil, nil
end

-- ============================================================================
-- INTERNAL: determine collection state given appearanceID + sourceID
-- ============================================================================
-- Returns one of: "COLLECTED", "KNOWN_OTHER", "NOT_COLLECTED", "NO_APPEARANCE"

local function _resolveState(appearanceID, sourceID)
    if not appearanceID then return "NO_APPEARANCE" end

    -- 1. Check the SPECIFIC sourceID for this itemID first. If THIS source is
    --    collected, the item is "COLLECTED" (we own this exact thing).
    local thisSourceCollected = false
    if sourceID and C_TransmogCollection and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
        local ok, has = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, sourceID)
        if ok and has then thisSourceCollected = true end
    end
    if thisSourceCollected then return "COLLECTED" end

    -- 2. Check if ANY source of this appearance is collected. If yes but it
    --    isn't sourceID, the appearance is "KNOWN_OTHER" (we have it from
    --    elsewhere).
    if C_TransmogCollection and C_TransmogCollection.GetAllAppearanceSources then
        local ok, allSources = pcall(C_TransmogCollection.GetAllAppearanceSources, appearanceID)
        if ok and allSources and C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance then
            for _, sid in ipairs(allSources) do
                local hasOk, has = pcall(C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance, sid)
                if hasOk and has then
                    -- One of the other sources is collected (sourceID itself
                    -- was already checked above and came back false)
                    return "KNOWN_OTHER"
                end
            end
        end
    end

    -- 3. Class-filtered fallback for clients where GetAllAppearanceSources
    --    isn't fully populated. GetAppearanceSources returns only sources
    --    the current class can wear, but a hit here still proves the player
    --    owns the appearance (and if it didn't match sourceID, it's "other").
    if C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
        local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, appearanceID)
        if ok and sources then
            for _, src in ipairs(sources) do
                if type(src) == "table" and src.isCollected then
                    if src.sourceID == sourceID then
                        return "COLLECTED"
                    else
                        return "KNOWN_OTHER"
                    end
                end
            end
        end
    end

    return "NOT_COLLECTED"
end

-- ============================================================================
-- INTERNAL: determine CanLearn given equipLoc / classID / subclassID
-- ============================================================================
-- Returns ok (bool), reason (string or nil)

local function _resolveCanLearn(equipLoc, classID, subclassID, itemID)
    -- Non-equippable items have no appearance to learn
    if not equipLoc or EXCLUDED_EQUIP_LOCS[equipLoc] then
        return false, "NOT_EQUIPPABLE"
    end
    if not classID then
        -- Item not loaded yet; treat as "maybe" but mark as unknown
        return true, nil
    end
    if not VALID_ITEM_CLASSES[classID] then
        return false, "WRONG_ITEM_CLASS"
    end

    -- Armor checks
    if classID == 4 then
        if ETB.BlizzardData then
            if subclassID == 6 then
                -- Shield: needs weapon proficiency for shield use
                if not ETB.BlizzardData:CanPlayerUseWeaponType("SHIELD") then
                    return false, "WRONG_WEAPON_TYPE"
                end
            else
                local armorType = ARMOR_SUBCLASS_MAP[subclassID]
                -- subclass 0 = misc armor (cloaks/shirts/tabards) — all classes wear
                if armorType and not ETB.BlizzardData:CanPlayerWearArmorType(armorType) then
                    return false, "WRONG_ARMOR_TYPE"
                end
            end
        end
        return true, nil
    end

    -- Weapon checks
    if classID == 2 then
        if ETB.BlizzardData and itemID then
            local weaponType = ETB.BlizzardData:GetWeaponType(itemID)
            if weaponType and not ETB.BlizzardData:CanPlayerUseWeaponType(weaponType) then
                return false, "WRONG_WEAPON_TYPE"
            end
        end
        return true, nil
    end

    return true, nil
end

-- ============================================================================
-- PUBLIC API
-- ============================================================================

-- Internal: produce + cache the full record for an itemID
local function _resolve(itemID)
    if not itemID then return nil end
    local c = getCache()
    if c then
        local cached = c:Get(itemID)
        if cached then return cached end
    end

    -- Item-info instant fetch (needed for equipLoc, classID, subclassID)
    local _, _, _, equipLoc, _, classID, subclassID = nil, nil, nil, nil, nil, nil, nil
    if GetItemInfoInstant then
        _, _, _, equipLoc, _, classID, subclassID = GetItemInfoInstant(itemID)
    end

    -- If item isn't loaded yet (no classID), don't cache — we want to retry
    -- once the data arrives. Return a "pending" record without storing it.
    if not classID then
        return {
            appearanceID = nil,
            sourceID = nil,
            state = nil,           -- nil = "data not loaded yet"
            canLearnOk = nil,
            canLearnReason = nil,
            pending = true,
        }
    end

    local appearanceID, sourceID = _resolveAppearance(itemID)
    local state = _resolveState(appearanceID, sourceID)
    local canLearnOk, canLearnReason = _resolveCanLearn(equipLoc, classID, subclassID, itemID)

    local record = {
        appearanceID = appearanceID,
        sourceID = sourceID,
        state = state,
        canLearnOk = canLearnOk,
        canLearnReason = canLearnReason,
        pending = false,
    }
    if c then c:Set(itemID, record) end
    return record
end

function CAPI:GetState(itemID)
    local r = _resolve(itemID)
    if not r or r.pending then return nil end
    return r.state
end

function CAPI:CanLearn(itemID)
    local r = _resolve(itemID)
    if not r then return nil, nil end
    if r.pending then return true, nil end  -- be permissive while loading
    return r.canLearnOk, r.canLearnReason
end

function CAPI:GetAppearance(itemID)
    local r = _resolve(itemID)
    if not r or r.pending then return nil, nil end
    return r.appearanceID, r.sourceID
end

function CAPI:Invalidate(itemID)
    if not itemID then return end
    local c = getCache()
    if c then c:Remove(itemID) end
end

function CAPI:ClearCache()
    local c = getCache()
    if c then c:Clear() end
end

function CAPI:GetStats()
    local c = getCache()
    if not c then return { name = "CollectionAPI", size = 0, maxSize = 500, hits = 0, misses = 0, hitRate = 0 } end
    return c:GetStats()
end

-- ============================================================================
-- EVENT WIRING
-- ============================================================================
-- Listens for collection-change events so we can invalidate proactively.
-- This is wired here (rather than in v15.5.0) because it costs nothing and
-- means callers that migrate later automatically get correct invalidation.

local eventFrame = CreateFrame("Frame", "ETBCollectionAPIEvents")
eventFrame:RegisterEvent("TRANSMOG_COLLECTION_SOURCE_ADDED")
eventFrame:RegisterEvent("TRANSMOG_COLLECTION_SOURCE_REMOVED")
eventFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
eventFrame:SetScript("OnEvent", function(_, event, arg1)
    if event == "TRANSMOG_COLLECTION_SOURCE_ADDED" or event == "TRANSMOG_COLLECTION_SOURCE_REMOVED" then
        -- Collection-state changed for some appearance; we don't know which
        -- itemIDs reference it, so the safe thing is a full clear.
        CAPI:ClearCache()
    elseif event == "GET_ITEM_INFO_RECEIVED" then
        -- arg1 is the itemID that just loaded; clear its pending record (if
        -- we cached one) so the next query refetches with valid data.
        if arg1 then CAPI:Invalidate(arg1) end
    end
end)
