--[[
============================================================================
ETB SourceLookup — Resolve item sources from JournalEncounter DB2 data
Chain: ETB.JLoot[itemID] → encounterID → ETB.JEncounters → ETB.JInstances
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB

ETB.SourceLookup = {}
local SL = ETB.SourceLookup

local DIFF_LABELS = {
    [1]  = "|cff00ff00Normal|r",
    [2]  = "|cffff8800Heroic|r",
    -- bit 4 (LFR) unused: no Raid Finder in MoP Classic
    [8]  = "|cffff00ffThunderforged|r",
    [15] = "|cffffffffAll Difficulties|r",
}

local EXP_NAMES = {
    [0] = "Classic", [1] = "TBC", [2] = "Wrath", [3] = "Cata", [4] = "MoP",
}

local _appearanceWarmQueued = false

local function RawBossLootReady()
    return ETB._BossLootLoader == nil and ETB.JLoot and ETB.JEncounters and ETB.JInstances
end

local function FullAppearanceDataReady()
    local SDA = ETB.SourceData_ATT
    if not SDA then return false end
    if SDA.IsAllLoaded then return SDA:IsAllLoaded() and true or false end
    if ETB.IsAppearanceDataReady then return ETB:IsAppearanceDataReady() and true or false end
    if SDA.IsLoaded then return SDA:IsLoaded() and true or false end
    return true
end

local function RequestFullAppearanceWarm()
    if _appearanceWarmQueued then return end
    _appearanceWarmQueued = true
    if ETB.WarmAppearanceData then
        ETB:WarmAppearanceData(nil, function() _appearanceWarmQueued = false end)
    else
        _appearanceWarmQueued = false
    end
end

-- Result cache (cleared on frame close to save memory)
SL._cache = {}
SL._cacheSize = 0
SL._sharedCache = {}   -- itemID -> array of distinct shared-appearance sources

-- ATT compact source `location` strings are space-stripped identifiers
-- ("SiegeofOrgrimmar") that the humaniser renders as "Siegeof Orgrimmar" — the
-- lowercase connector never splits. JInstances holds the authoritative display
-- name ("Siege of Orgrimmar"), so reconcile to it by a punctuation/space-stripped
-- key. Falls back to the original string when there's no JInstances match.
SL._instCanonIndex = nil
local function instNKey(s)
    if type(s) ~= "string" then return nil end
    return (s:lower():gsub("[^%w]", ""))
end
function SL:CanonicalInstanceName(name)
    if type(name) ~= "string" or name == "" then return name end
    if not self._instCanonIndex then
        self._instCanonIndex = {}
        if RawBossLootReady() then
            -- Force lazy materialisation: pairs() won't fire the proxy __index in
            -- Lua 5.1, and the loader rebinds ETB.JInstances to the real table.
            local _trigger = ETB.JInstances[1]
            for _, inst in pairs(ETB.JInstances) do
                if type(inst) == "table" and type(inst[1]) == "string" then
                    self._instCanonIndex[instNKey(inst[1])] = inst[1]
                end
            end
        end
    end
    return self._instCanonIndex[instNKey(name)] or name
end

function SL:Resolve(encID, diffMask, facMask)
    if not RawBossLootReady() then return nil end
    local enc = ETB.JEncounters and ETB.JEncounters[encID]
    if not enc then return nil end
    
    local bossName = enc[1]
    local instanceID = enc[2]
    local inst = ETB.JInstances and ETB.JInstances[instanceID]
    if not inst then return nil end
    
    -- Build difficulty string
    local diffText
    if diffMask == 15 or diffMask == 0 then
        diffText = nil  -- All difficulties, don't show
    else
        local parts = {}
        if bit.band(diffMask, 1) > 0 then parts[#parts + 1] = "Normal" end
        if bit.band(diffMask, 2) > 0 then parts[#parts + 1] = "Heroic" end
        if bit.band(diffMask, 8) > 0 then parts[#parts + 1] = "Thunderforged" end
        diffText = #parts > 0 and table.concat(parts, "/") or nil
    end
    
    return {
        bossName = bossName,
        instanceName = inst[1],
        expansionID = inst[2],
        expansion = EXP_NAMES[inst[2]] or ("Exp" .. inst[2]),
        isRaid = inst[3] == 1,
        difficulty = diffText,
        diffMask = diffMask,
        faction = facMask == 1 and "Horde" or (facMask == 2 and "Alliance" or nil),
    }
end

function SL:GetItemSources(itemID)
    if not itemID then return nil end
    
    -- Cache hit
    if self._cache[itemID] then return self._cache[itemID] end
    
    local hasRawBossLoot = RawBossLootReady()
    local lootData = hasRawBossLoot and ETB.JLoot[itemID] or nil
    if not lootData then
        -- Compact data fallback
        if FullAppearanceDataReady() and ETB.SourceData_ATT.GetPrimarySource then
            local attSrc = ETB.SourceData_ATT:GetPrimarySource(itemID)
            if attSrc then
                local bossName     = attSrc.name
                local instanceName = attSrc.location
                -- If the boss came back "Unknown" but we have an encounterID,
                -- resolve the real boss (and instance) authoritatively from
                -- JEncounters — the same graph the primary path uses. This fills
                -- in bosses for items that aren't in JLoot but do carry an encID.
                if (not bossName or bossName == "" or bossName == "Unknown") and attSrc.encounterID and RawBossLootReady() then
                    local enc = ETB.JEncounters and ETB.JEncounters[attSrc.encounterID]
                    if enc then
                        bossName = enc[1] or bossName
                        local inst = enc[2] and ETB.JInstances and ETB.JInstances[enc[2]]
                        if inst and inst[1] then instanceName = inst[1] end
                    end
                end
                local result = {{
                    bossName = bossName,
                    instanceName = self:CanonicalInstanceName(instanceName),
                    expansion = attSrc.expansion,
                    isRaid = attSrc.isRaid,
                    difficulty = nil,
                }}
                self._cache[itemID] = result
                self._cacheSize = self._cacheSize + 1
                return result
            end
        elseif ETB.SourceData_ATT then
            RequestFullAppearanceWarm()
        end
        return nil
    end
    
    local results = {}
    
    -- Single source: {encID, diff[, fac]}
    -- Multiple: {{encID,diff},{encID,diff}}
    if type(lootData[1]) == "table" then
        for _, entry in ipairs(lootData) do
            local r = self:Resolve(entry[1], entry[2] or 15, entry[3] or 0)
            if r then results[#results + 1] = r end
        end
    else
        local r = self:Resolve(lootData[1], lootData[2] or 15, lootData[3] or 0)
        if r then results[#results + 1] = r end
    end
    
    if #results > 0 then
        self._cache[itemID] = results
        self._cacheSize = self._cacheSize + 1
        -- Keep cache bounded
        if self._cacheSize > 2000 then
            wipe(self._cache)
            self._cacheSize = 0
        end
        return results
    end
    return nil
end

-- Classify the "variant" of an item's source so players can tell apart the
-- different versions that share one appearance: e.g. the Celestial (vendor /
-- Timeless Isle) version vs. the Normal vs. Heroic boss drops. MoP Classic has
-- no LFR, so old LFR looks now come from Celestial gear — a genuinely different
-- acquisition path. Returns a short label, or nil when nothing distinctive.
function SL:ClassifyVariant(itemID, resolved)
    local diff = resolved and resolved.difficulty
    if FullAppearanceDataReady() and ETB.SourceData_ATT.GetPrimarySource then
        local okP, prim = pcall(function() return ETB.SourceData_ATT:GetPrimarySource(itemID) end)
        if okP and prim then
            local nm  = prim.name or ""
            local loc = prim.location or ""
            if nm == "Celestial" or loc == "Celestial" then return "Celestial" end
            if nm:find("Timeless") or loc:find("Timeless") then return "Timeless Isle" end
            if prim.isWorldBoss then return "World Boss" end
            if prim.type == "QUEST" then return "Quest" end
            if prim.type == "PROFESSION" then return "Crafted" end
            if prim.type == "VENDOR" then return diff or "Vendor" end
            if prim.difficulty then return prim.difficulty end
        end
    elseif ETB.SourceData_ATT then
        RequestFullAppearanceWarm()
    end
    return diff
end

-- All distinct obtainable sources for an item's APPEARANCE, across every item
-- that shares it. This is what lets the farming guide show a player every place
-- they can hunt a look — so a locked boss never hides an open alternative.
--
-- Uses the class-filtered appearance sources (the visuals this character can
-- actually collect), resolves each shared item to a boss/instance via the same
-- GetItemSources graph, and dedupes by instance|boss. Cached per item; shared
-- sources are static, so the cache lives for the session.
-- Returns an array of { itemID, instName, encName, isRaid }.
function SL:GetSharedItemSources(itemID)
    if not itemID then return {} end
    if self._sharedCache[itemID] then return self._sharedCache[itemID] end

    local out, seen = {}, {}
    local DISTINCT_CAP, SCAN_CAP, scanned = 16, 80, 0
    if C_TransmogCollection and C_TransmogCollection.GetItemInfo
       and C_TransmogCollection.GetAppearanceSources then
        local okV, visualID = pcall(C_TransmogCollection.GetItemInfo, itemID)
        if okV and visualID and visualID > 0 then
            local okS, sources = pcall(C_TransmogCollection.GetAppearanceSources, visualID)
            if okS and sources then
                for _, src in ipairs(sources) do
                    if scanned >= SCAN_CAP or #out >= DISTINCT_CAP then break end
                    scanned = scanned + 1
                    local altItem = (type(src) == "table") and src.itemID or nil
                    if not altItem then
                        local sourceID = (type(src) == "table") and src.sourceID or src
                        if sourceID and C_TransmogCollection.GetSourceInfo then
                            local okI, info = pcall(C_TransmogCollection.GetSourceInfo, sourceID)
                            if okI and info then altItem = info.itemID end
                        end
                    end
                    if altItem then
                        local resolved = self:GetItemSources(altItem)
                        if resolved and resolved[1] and resolved[1].instanceName then
                            local s = resolved[1]
                            local key = (s.instanceName or "?") .. "|" .. (s.bossName or "?")
                            if not seen[key] then
                                seen[key] = true
                                out[#out + 1] = {
                                    itemID   = altItem,
                                    instName = s.instanceName,
                                    encName  = s.bossName,
                                    isRaid   = s.isRaid,
                                    variant  = self:ClassifyVariant(altItem, s),
                                }
                            end
                        end
                    end
                end
            end
        end
    end

    self._sharedCache[itemID] = out
    return out
end

-- Quick single-line source text
function SL:GetSourceText(itemID)
    local sources = self:GetItemSources(itemID)
    if not sources or #sources == 0 then return nil end
    local s = sources[1]
    local text = s.bossName or ""
    if s.instanceName then
        text = text .. (text ~= "" and " - " or "") .. s.instanceName
    end
    if s.difficulty then
        text = text .. " [" .. s.difficulty .. "]"
    end
    return text
end

-- Colored source text for UI display
function SL:GetSourceTextColored(itemID)
    local sources = self:GetItemSources(itemID)
    if not sources or #sources == 0 then return nil end
    local s = sources[1]
    local text = ""
    if s.instanceName then
        text = "|cffffcc00" .. s.instanceName .. "|r"
        if s.isRaid then text = text .. " |cffff8800(Raid)|r" end
    end
    if s.bossName then
        text = text .. (text ~= "" and " - " or "") .. "|cffffffff" .. s.bossName .. "|r"
    end
    if s.difficulty then
        text = text .. " |cff888888[" .. s.difficulty .. "]|r"
    end
    return text
end

function SL:ClearCache()
    wipe(self._cache)
    self._cacheSize = 0
end

ETB:Debug("SourceLookup", "SourceLookup loaded")
