--[[
============================================================================
ETB - SourceDB.lua (Source Enrichment Layer)
============================================================================
Provides boss names, instance keys, and source metadata for 18,845 items
from merged source enrichment data.

Used by MapperBridge as enrichment when its own data is missing
encounter names, instance names, or source details.

ARCHITECTURE:
  - Data files (Source_Classic.lua ... Source_MoP.lua) register loader
    functions on NS._SrcLoaders[expansion]
  - On first access, loaders run and return string tables + item arrays
  - Items stored as compact arrays, decoded on demand with LRU cache
  - Zero memory impact until first query

USAGE (internal to MapperBridge):
  local info = ETB.SourceDB:Get(itemID)
  if info then
      info.bossName       -- "Ook-Ook"
      info.instanceKey    -- "StormstoutBrewery"
      info.instanceID     -- 302 (numeric, for Names.inst lookup)
      info.instanceName   -- "Deadmines" (readable, for items without instanceKey)
      info.encounterID    -- 668
      info.ejInstanceID   -- 63 (Encounter Journal instance ID)
      info.mapID          -- 5963
      info.expansion      -- "MoP"
      info.sourceType     -- "LOOT"
      info.isDungeon      -- true
      info.isRaid         -- false
      info.isTrash        -- false
      info.isWorldBoss    -- false
      info.isCrafted      -- false
      info.recipeItemID   -- nil or recipe item ID
  end
============================================================================
]]--

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

-- Loader registration table (data files write here)
NS._SrcLoaders = NS._SrcLoaders or {}

-- Public API
ETB.SourceDB = {}
local SDB = ETB.SourceDB

---------------------------------------------------------------------------
-- Constants
---------------------------------------------------------------------------

local SOURCE_TYPES = {
    [0]="UNKNOWN",[1]="LOOT",[2]="QUEST",[3]="VENDOR",[4]="FIRST_AID",
    [5]="BLACKSMITHING",[6]="LEATHERWORKING",[7]="ALCHEMY",
    [9]="COOKING",[10]="MINING",[11]="TAILORING",[12]="ENGINEERING",
    [13]="ENCHANTING",[14]="FISHING",[16]="UNKNOWN_16",
    [17]="JEWELCRAFTING",[18]="INSCRIPTION",[19]="ARCHAEOLOGY",
}

local PROFESSION_SOURCES = {
    [4]=true,[5]=true,[6]=true,[7]=true,[9]=true,[10]=true,
    [11]=true,[12]=true,[13]=true,[14]=true,[17]=true,[18]=true,[19]=true,
}

local FLAG_DUNGEON = 0x01
local FLAG_RAID    = 0x02
local FLAG_TRASH   = 0x04
local FLAG_WBOSS   = 0x08

local EXPANSIONS = { "Classic", "TBC", "Wrath", "Cata", "MoP" }

---------------------------------------------------------------------------
-- Internal State
---------------------------------------------------------------------------

local loaded = {}           -- expansion -> {instances, bosses, items}
local itemExpansion = {}    -- itemID -> expansion
local indexBuilt = false

-- LRU cache for decoded records
local cache = {}
local cacheOrder = {}
local CACHE_MAX = 200

---------------------------------------------------------------------------
-- Data Loading (lazy)
---------------------------------------------------------------------------

local function LoadExpansion(expansion)
    if loaded[expansion] then return true end
    local loader = NS._SrcLoaders and NS._SrcLoaders[expansion]
    if not loader or type(loader) ~= "function" then return false end
    
    local I, B, D, N = loader()
    if not D then return false end
    
    loaded[expansion] = { instances = I or {}, bosses = B or {}, items = D, instNames = N or {} }
    NS._SrcLoaders[expansion] = true  -- Free the loader
    return true
end

local function BuildIndex()
    if indexBuilt then return end
    local n = 0
    for _, exp in ipairs(EXPANSIONS) do
        if LoadExpansion(exp) then
            for itemID in pairs(loaded[exp].items) do
                itemExpansion[itemID] = exp
                n = n + 1
                if n % 2000 == 0 and ETB.YieldSlice then ETB:YieldSlice() end
            end
        end
    end
    indexBuilt = true
end

local function FindExpansionForItem(itemID)
    if itemExpansion[itemID] then return itemExpansion[itemID] end
    for _, exp in ipairs(EXPANSIONS) do
        if LoadExpansion(exp) then
            local expData = loaded[exp]
            if expData and expData.items and expData.items[itemID] then
                itemExpansion[itemID] = exp
                return exp
            end
        end
    end
    return nil
end

---------------------------------------------------------------------------
-- Record Decoding
---------------------------------------------------------------------------

local function DecodeRecord(expansion, itemID, raw)
    local expData = loaded[expansion]
    if not expData then return nil end
    
    local srcTypeID   = raw[1] or 0
    local instIdx     = raw[2] or 0
    local bossIdx     = raw[3] or 0
    local encounterID = raw[4] or 0
    local bossNameIdx = raw[5] or 0
    local mapID       = raw[6] or 0
    local difficulty  = raw[7] or 0
    local flags       = raw[8] or 0
    local recipeID    = raw[9] or 0
    local instanceID  = raw[10] or 0
    local ejInstID    = raw[11] or 0
    local instNameIdx = raw[12] or 0
    
    local rec = {
        itemID       = itemID,
        expansion    = expansion,
        sourceType   = SOURCE_TYPES[srcTypeID] or "UNKNOWN",
        sourceTypeID = srcTypeID,
    }
    
    if instIdx > 0 and expData.instances[instIdx] then
        rec.instanceKey = expData.instances[instIdx]
    end
    if bossIdx > 0 then rec.bossIdx = bossIdx end
    if bossNameIdx > 0 and expData.bosses[bossNameIdx] then
        rec.bossName = expData.bosses[bossNameIdx]
    end
    if encounterID > 0 then rec.encounterID = encounterID end
    if mapID > 0 then rec.mapID = mapID end
    if difficulty > 0 then rec.difficulty = difficulty end
    if recipeID > 0 then rec.recipeItemID = recipeID end
    if instanceID > 0 then rec.instanceID = instanceID end
    if ejInstID > 0 then rec.ejInstanceID = ejInstID end
    if instNameIdx > 0 and expData.instNames[instNameIdx] then
        rec.instanceName = expData.instNames[instNameIdx]
    end
    
    rec.isDungeon   = bit.band(flags, FLAG_DUNGEON) > 0
    rec.isRaid      = bit.band(flags, FLAG_RAID) > 0
    rec.isTrash     = bit.band(flags, FLAG_TRASH) > 0
    rec.isWorldBoss = bit.band(flags, FLAG_WBOSS) > 0
    rec.isCrafted   = PROFESSION_SOURCES[srcTypeID] or false
    
    return rec
end

---------------------------------------------------------------------------
-- Cache
---------------------------------------------------------------------------

local function CacheGet(itemID)
    return cache[itemID]
end

local function CachePut(itemID, record)
    if cache[itemID] then return end
    if #cacheOrder >= CACHE_MAX then
        local evictID = table.remove(cacheOrder, 1)
        cache[evictID] = nil
    end
    cache[itemID] = record
    table.insert(cacheOrder, itemID)
end

---------------------------------------------------------------------------
-- PUBLIC API
---------------------------------------------------------------------------

--- Get enrichment data for an item.
-- @param itemID number
-- @return table|nil
function SDB:Get(itemID)
    if not itemID then return nil end
    
    local cached = CacheGet(itemID)
    if cached then return cached end
    
    local expansion = FindExpansionForItem(itemID)
    if not expansion then return nil end
    
    local expData = loaded[expansion]
    if not expData then return nil end
    
    local raw = expData.items[itemID]
    if not raw then return nil end
    
    local record = DecodeRecord(expansion, itemID, raw)
    if record then CachePut(itemID, record) end
    return record
end

--- Quick existence check.
function SDB:Has(itemID)
    if not itemID then return false end
    return FindExpansionForItem(itemID) ~= nil
end

--- Get boss name for an item (most common enrichment).
function SDB:GetBossName(itemID)
    local rec = self:Get(itemID)
    return rec and rec.bossName or nil
end

--- Get instance key for an item.
function SDB:GetInstanceKey(itemID)
    local rec = self:Get(itemID)
    return rec and rec.instanceKey or nil
end

--- Get item count.
function SDB:GetCount()
    if not indexBuilt then BuildIndex() end
    local count = 0
    for _ in pairs(itemExpansion) do count = count + 1 end
    return count
end

--- Get items for an instance key.
function SDB:GetByInstance(instanceKey)
    if not indexBuilt then BuildIndex() end
    local result = {}
    for _, exp in ipairs(EXPANSIONS) do
        if loaded[exp] then
            local instIdx
            for i, key in ipairs(loaded[exp].instances) do
                if key == instanceKey then instIdx = i; break end
            end
            if instIdx then
                for itemID, raw in pairs(loaded[exp].items) do
                    if (raw[2] or 0) == instIdx then
                        result[#result + 1] = itemID
                    end
                end
            end
        end
    end
    return result
end

--- Get items for an encounter.
function SDB:GetByEncounter(encounterID)
    if not indexBuilt then BuildIndex() end
    local result = {}
    for _, exp in ipairs(EXPANSIONS) do
        if loaded[exp] then
            for itemID, raw in pairs(loaded[exp].items) do
                if (raw[4] or 0) == encounterID then
                    result[#result + 1] = itemID
                end
            end
        end
    end
    return result
end

--- Get stats.
function SDB:GetStats()
    if not indexBuilt then BuildIndex() end
    local byExp = {}
    local total = 0
    for _, exp in ipairs(EXPANSIONS) do
        if loaded[exp] then
            local count = 0
            for _ in pairs(loaded[exp].items) do count = count + 1 end
            byExp[exp] = count
            total = total + count
        end
    end
    return { total = total, byExpansion = byExp }
end

--- Force load all data.
function SDB:LoadAll()
    if self._loaded then return end
    for _, exp in ipairs(EXPANSIONS) do
        LoadExpansion(exp)
        if ETB.YieldSlice then ETB:YieldSlice() end  -- yield between per-expansion materializations
    end
    BuildIndex()
    self._loaded = true
end

--- Clear decode cache.
function SDB:ClearCache()
    cache = {}
    cacheOrder = {}
end

SDB.SOURCE_TYPES = SOURCE_TYPES
SDB.EXPANSIONS = EXPANSIONS

--- Get all items flagged as world boss loot.
-- Returns a flat array of {itemID=, encounterID=} entries for BossLootTooltip.
-- Scans raw data directly to avoid decoding every item (only checks flags field).
function SDB:GetAllWorldBossItems()
    if not indexBuilt then BuildIndex() end
    local result = {}
    local n = 0
    for _, exp in ipairs(EXPANSIONS) do
        if loaded[exp] then
            for itemID, raw in pairs(loaded[exp].items) do
                n = n + 1
                if n % 2000 == 0 and ETB.YieldSlice then ETB:YieldSlice() end
                local flags = raw[8] or 0
                if bit.band(flags, FLAG_WBOSS) > 0 then
                    local encounterID = raw[4]
                    if encounterID and encounterID ~= 0 then
                        result[#result + 1] = { itemID = itemID, encounterID = encounterID }
                    end
                end
            end
        end
    end
    return result
end

--- Get all loot-flagged items with encounterIDs (general boss loot supplement).
-- Returns a flat array of {itemID=, encounterID=, instanceID=, isTrash=, isRaid=, isDungeon=}.
-- Used by BossLootTooltip to supplement compact data — recovers ~579 MoP items
-- that compact data lacks encounter mappings for.
function SDB:GetAllLootItems()
    if not indexBuilt then BuildIndex() end
    local result = {}
    local n = 0
    for _, exp in ipairs(EXPANSIONS) do
        if loaded[exp] then
            for itemID, raw in pairs(loaded[exp].items) do
                n = n + 1
                if n % 2000 == 0 and ETB.YieldSlice then ETB:YieldSlice() end
                local srcTypeID  = raw[1] or 0
                local encounterID = raw[4] or 0
                local flags       = raw[8] or 0
                local instanceID  = raw[10] or 0
                -- Include items that are LOOT type (srcType=1) with an encounterID,
                -- OR any item with an encounterID that isn't a profession source.
                -- Exclude world boss items (handled separately in Pass 1.5).
                local isWorldBoss = bit.band(flags, FLAG_WBOSS) > 0
                if encounterID > 0 and not isWorldBoss and not PROFESSION_SOURCES[srcTypeID] then
                    result[#result + 1] = {
                        itemID      = itemID,
                        encounterID = encounterID,
                        instanceID  = instanceID > 0 and instanceID or nil,
                        isTrash     = bit.band(flags, FLAG_TRASH) > 0,
                        isRaid      = bit.band(flags, FLAG_RAID) > 0,
                        isDungeon   = bit.band(flags, FLAG_DUNGEON) > 0,
                    }
                end
            end
        end
    end
    return result
end

--- Get set of all item IDs known to SourceDB (for audit coverage checks).
-- Returns { [itemID] = expansion } for every loaded item.
function SDB:GetAllItemIDs()
    if not indexBuilt then BuildIndex() end
    local result = {}
    for id, exp in pairs(itemExpansion) do
        result[id] = exp
    end
    return result
end
