--[[
============================================================================
Modules/BossLootTooltip/Index.lua  (added in v15.1.0 — Audit Batch 4 Part 1)
============================================================================

NPC-loot index builder for the BossLootTooltip orchestrator. Owner-pattern:
each method takes `owner` (the BLT orchestrator) as first argument and
accesses BLT state via owner.npcIndex, owner.stats, owner.indexBuilt, etc.

Methods owned here (3):
  BuildIndex          — primary index builder (~550 lines). Reads compact
                        data from Mapper, JLoot, RareMobLoot; resolves
                        encounter->NPC and instance->NPCs lookups; populates
                        owner.npcIndex[npcID] = { [itemID] = flags }
  GetNPCIDFromGUID    — extract numeric NPC ID from a unit GUID string
  BuildJLootIndex     — secondary index for JLoot (encounterID-keyed,
                        used as fallback when compact data has no match)

Index-local constants (NOT shared with other submodules):
  ST_BOSS, ST_QUEST, ST_RARE — source-type indices from compact data

External dependencies pulled from owner (the BLT orchestrator):
  owner.npcIndex, owner.indexBuilt, owner.building, owner.stats,
  owner._jlootByEnc, owner.FLAG_*, owner.VALID_ITEM_CLASSES,
  owner.EXCLUDED_EQUIP_LOCS, owner.WORLD_BOSS_ENC_TO_NPC,
  owner.WORLD_BOSS_NPC_SET, owner.AddToNPCIndex

Closure / shadowing notes: No function() closures, all synchronous.
Verified clean by bare-upvalue audit in test_v15_1_extraction.lua.
============================================================================
]]--
local ADDON_NAME, NS = ...
local ETB = NS.ETB
NS.ETB.BossLootTooltip = NS.ETB.BossLootTooltip or {}
NS.ETB.BossLootTooltip.Index = NS.ETB.BossLootTooltip.Index or {}
local Index = NS.ETB.BossLootTooltip.Index

-- =============================================================================
-- FILE-LOCAL CONSTANTS
-- =============================================================================
-- Source-type indices from Compact/Strings.lua ST table.
-- ST = {"ACHIEVEMENT","BOSS","CRAFTED","EVENT","OTHER","PVP","QUEST","RARE_MOB",
--       "REMOVED","REPUTATION","SCENARIO","VENDOR","WORLD_DROP"}
local ST_BOSS = 2
local ST_QUEST = 7
local ST_RARE = 8

-- =============================================================================
-- METHODS (extracted from Modules/BossLootTooltip.lua in v15.1.0)
-- =============================================================================

function Index:BuildIndex(owner)
    if owner.indexBuilt or owner.building then return end
    owner.building = true
    
    local SDA = ETB.SourceData_ATT
    if not SDA then
        ETB:Debug("BossLoot", "SourceData_ATT not available")
        owner.building = false
        return
    end
    
    -- Ensure MapperBridge name table is loaded (needed for NPC names)
    if SDA.EnsureLoaded then
        SDA:EnsureLoaded()
    end
    
    local compact = NS.Compact
    if not compact then
        ETB:Debug("BossLoot", "Compact data not available")
        owner.building = false
        return
    end
    
    local npcIndex = {}             -- npcID → { [itemID] = flag }
    local encToNPCs = {}            -- encounterID → { [npcID] = true } (MULTI-NPC AWARE)
    local instanceNPCs = {}         -- instanceID → { [npcID] = true }
    local pendingEncounter = {}     -- items with encounter but no NPC
    local pendingInstance = {}      -- BOSS items with instance but no NPC/encounter
    
    local stats = { directNPC = 0, encounterResolved = 0, instanceGrouped = 0,
                    worldBoss = 0, questNPC = 0, rareMob = 0 }
    
    local expansions = { "Classic", "TBC", "Wrath", "Cata", "MoP" }
    
    -- ========================================================================
    -- PRE-SEED: World boss encounter→NPC mappings
    -- ========================================================================
    
    for encID, npcID in pairs(owner.WORLD_BOSS_ENC_TO_NPC) do
        if not encToNPCs[encID] then encToNPCs[encID] = {} end
        encToNPCs[encID][npcID] = true
    end
    
    -- PRE-SEED: JEncToNpcs from BossLoot.lua (multi-NPC aware)
    if ETB._BossLootLoader then ETB._BossLootLoader(); ETB._BossLootLoader = nil end
    if ETB.JEncToNpcs then
        for encID, npcData in pairs(ETB.JEncToNpcs) do
            if not encToNPCs[encID] then encToNPCs[encID] = {} end
            if type(npcData) == "table" then
                for _, npc in ipairs(npcData) do
                    encToNPCs[encID][npc] = true
                end
            else
                encToNPCs[encID][npcData] = true
            end
        end
    end
    
    -- ========================================================================
    -- PASS 1: Build direct NPC index + encounter→NPC lookup + collect pending
    -- ONLY index BOSS (2) and RARE_MOB (8) source types — skip vendors,
    -- quest givers, world drops, crafted, PVP, etc. Those aren't "loot."
    -- ========================================================================
    
    for _, expName in ipairs(expansions) do
        local data = compact[expName]
        if data and data.I then
            for itemID, raw in pairs(data.I) do
                if type(raw) == "table" then
                    -- Synchronous class check (no async)
                    local _, _, _, equipLoc, _, classID = GetItemInfoInstant(itemID)
                    
                    -- Skip non-armor/weapon and non-visible slots (trinkets, rings, necks)
                    if classID and owner.VALID_ITEM_CLASSES[classID] and not (equipLoc and owner.EXCLUDED_EQUIP_LOCS[equipLoc]) then
                        local npcID = raw[2]
                        local st = raw[3]       -- source type index
                        local eID = raw[4]      -- encounter ID
                        local iID = raw[5]      -- instance ID
                        
                        -- ONLY process BOSS and RARE_MOB items for the loot tooltip
                        -- Vendor (12), Quest (7), World Drop (13), Crafted (3), PVP (6),
                        -- Event (4), Achievement (1), etc. are NOT boss loot.
                        -- EXCEPTION: QUEST items from known world boss NPCs (coded as
                        -- quest rewards but actually world boss drops)
                        local isWorldBossQuest = (st == ST_QUEST and npcID and owner.WORLD_BOSS_NPC_SET[npcID])
                        if st ~= ST_BOSS and st ~= ST_RARE and not isWorldBossQuest then
                            -- skip entirely
                        
                        -- Skip confirmed world drops (marked as BOSS but drop from 20+ creatures)
                        elseif ETB.WorldDropIndex and ETB.WorldDropIndex[itemID] then
                            -- skip: confirmed world drop, not boss loot
                        else
                        
                        -- Normalize zeros
                        if npcID == 0 then npcID = nil end
                        if eID == 0 then eID = nil end
                        if iID == 0 then iID = nil end
                        
                        -- Detect token cost in field 27
                        local hasToken = false
                        local costField = raw[27]
                        if costField and type(costField) == "table" then
                            for _, c in ipairs(costField) do
                                if type(c) == "table" and c[1] == "i" then
                                    hasToken = true
                                    break
                                end
                            end
                        end
                        
                        if npcID then
                            -- Direct NPC mapping
                            local flag = hasToken and (owner.FLAG_DIRECT + owner.FLAG_TOKEN) or owner.FLAG_DIRECT
                            owner.AddToNPCIndex(npcIndex, npcID, itemID, flag)
                            stats.directNPC = stats.directNPC + 1
                            
                            -- Build encounter→NPCs and instance→NPCs lookups
                            if eID then
                                if not encToNPCs[eID] then encToNPCs[eID] = {} end
                                encToNPCs[eID][npcID] = true
                            end
                            if iID then
                                if not instanceNPCs[iID] then instanceNPCs[iID] = {} end
                                instanceNPCs[iID][npcID] = true
                            end
                            
                        elseif eID then
                            -- Has encounter but no NPC - queue for pass 2
                            pendingEncounter[#pendingEncounter + 1] = {
                                itemID = itemID, eID = eID, iID = iID, hasToken = hasToken
                            }
                            
                        elseif iID then
                            -- BOSS items with instance but no NPC/encounter
                            -- Instance grouping assigns them to ALL known bosses
                            -- in that instance. Token items get priority flag.
                            pendingInstance[#pendingInstance + 1] = {
                                itemID = itemID, iID = iID, hasToken = hasToken
                            }
                        end
                        
                        -- Process additional sources (field 26)
                        -- Only if they are BOSS-type sources
                        local srcs = raw[26]
                        if srcs and type(srcs) == "table" then
                            for _, src in ipairs(srcs) do
                                if type(src) == "table" then
                                    local srcST = src[4] or 0
                                    -- Only process BOSS or RARE additional sources
                                    if srcST == ST_BOSS or srcST == ST_RARE or srcST == 0 then
                                        local srcEID = (src[1] and src[1] ~= 0) and src[1] or nil
                                        local srcIID = (src[2] and src[2] ~= 0) and src[2] or nil
                                        local srcNPC = (src[3] and src[3] ~= 0) and src[3] or nil
                                        
                                        if srcNPC then
                                            owner.AddToNPCIndex(npcIndex, srcNPC, itemID, owner.FLAG_DIRECT)
                                            if srcEID then if not encToNPCs[srcEID] then encToNPCs[srcEID] = {} end; encToNPCs[srcEID][srcNPC] = true end
                                            if srcIID then
                                                if not instanceNPCs[srcIID] then instanceNPCs[srcIID] = {} end
                                                instanceNPCs[srcIID][srcNPC] = true
                                            end
                                        elseif srcEID then
                                            pendingEncounter[#pendingEncounter + 1] = {
                                                itemID = itemID, eID = srcEID, hasToken = hasToken
                                            }
                                        end
                                    end
                                end
                            end
                        end
                        
                        end -- end st == BOSS or RARE check
                    end
                end
            end
        end
    end
    
    -- ========================================================================
    -- PASS 1.5: SourceDB world boss recovery
    -- World boss items (Sha of Anger, Galleon, Nalak, Oondasta, Celestials,
    -- Ordos) are coded as QUEST (ST=7) in MoP compact data. SourceDB
    -- correctly flags them with isWorldBoss=true and has encounter IDs.
    -- ========================================================================
    
    -- Lazy-load SourceDB on first BuildIndex (deferred from login to save memory)
    if ETB.SourceDB and not ETB.SourceDB._loaded then
        ETB.SourceDB:LoadAll()
        ETB:Debug("BossLoot", "SourceDB loaded on demand")
    end
    
    if ETB.SourceDB and ETB.SourceDB.GetAllWorldBossItems then
        local wbItems = ETB.SourceDB:GetAllWorldBossItems()
        if wbItems then
            for _, wbEntry in ipairs(wbItems) do
                local itemID = wbEntry.itemID
                local encID = wbEntry.encounterID
                if itemID and encID then
                    local _, _, _, equipLoc, _, classID = GetItemInfoInstant(itemID)
                    if classID and owner.VALID_ITEM_CLASSES[classID] and not (equipLoc and owner.EXCLUDED_EQUIP_LOCS[equipLoc]) then
                        -- Resolve to ALL NPCs for this encounter (multi-NPC aware)
                        local npcs = encToNPCs[encID]
                        if npcs then
                            for npc in pairs(npcs) do
                                if owner.AddToNPCIndex(npcIndex, npc, itemID, owner.FLAG_DIRECT + owner.FLAG_WORLDBOSS) then
                                    stats.worldBoss = stats.worldBoss + 1
                                end
                            end
                        else
                            pendingEncounter[#pendingEncounter + 1] = {
                                itemID = itemID, eID = encID, hasToken = false
                            }
                        end
                    end
                end
            end
        end
    end
    
    -- (Vendor-scan fallback removed: MoP world boss items are coded as QUEST,
    -- not VENDOR. Pass 1 already catches them via isWorldBossQuest.)
    
    -- ========================================================================
    -- PASS 1.6: SourceDB general loot supplement
    -- Ingests all SourceDB loot items with encounterIDs — not just world bosses.
    -- Recovers ~579 MoP items that compact data lacks encounter mappings for.
    -- Items already in npcIndex (via Pass 1 direct NPC) are skipped.
    -- Items with encounterIDs feed into encToNPCs for Pass 2 resolution.
    -- ========================================================================
    
    local sdbLootAdded = 0
    if ETB.SourceDB and ETB.SourceDB.GetAllLootItems then
        local lootItems = ETB.SourceDB:GetAllLootItems()
        if lootItems then
            for _, lootEntry in ipairs(lootItems) do
                local itemID = lootEntry.itemID
                local encID = lootEntry.encounterID
                if itemID and encID then
                    -- Skip if already indexed via any NPC
                    local alreadyIndexed = false
                    for _, items in pairs(npcIndex) do
                        if items[itemID] then alreadyIndexed = true; break end
                    end
                    if not alreadyIndexed then
                        local _, _, _, equipLoc, _, classID = GetItemInfoInstant(itemID)
                        if classID and owner.VALID_ITEM_CLASSES[classID] and not (equipLoc and owner.EXCLUDED_EQUIP_LOCS[equipLoc]) then
                            -- Try direct resolution via encToNPCs
                            local npcs = encToNPCs[encID]
                            if npcs then
                                for npc in pairs(npcs) do
                                    if owner.AddToNPCIndex(npcIndex, npc, itemID, owner.FLAG_ENCOUNTER) then
                                        sdbLootAdded = sdbLootAdded + 1
                                    end
                                end
                            else
                                -- Queue for Pass 2 encounter resolution
                                pendingEncounter[#pendingEncounter + 1] = {
                                    itemID = itemID, eID = encID,
                                    iID = lootEntry.instanceID, hasToken = false
                                }
                            end
                        end
                    end
                end
            end
        end
        ETB:Debug("BossLoot", string.format("SourceDB loot supplement: %d new item-NPC mappings", sdbLootAdded))
    end
    
    -- ========================================================================
    -- PASS 2: Resolve encounter-only items via encounter→NPCs lookup
    -- Uses: encToNPCs (set-based), world boss table, JNpcToEnc reverse.
    -- NOTE: EJ_GetCreatureInfo returns journal section IDs in MoP 5.4.8,
    -- NOT actual game npcIDs. Do not use it for NPC resolution.
    -- ========================================================================
    
    for _, pending in ipairs(pendingEncounter) do
        local npcs = encToNPCs[pending.eID]
        
        if npcs then
            -- Resolve to ALL NPCs for this encounter
            local flag = pending.hasToken and (owner.FLAG_ENCOUNTER + owner.FLAG_TOKEN) or owner.FLAG_ENCOUNTER
            for npc in pairs(npcs) do
                if owner.AddToNPCIndex(npcIndex, npc, pending.itemID, flag) then
                    stats.encounterResolved = stats.encounterResolved + 1
                end
                if pending.iID then
                    if not instanceNPCs[pending.iID] then instanceNPCs[pending.iID] = {} end
                    instanceNPCs[pending.iID][npc] = true
                end
            end
        end
    end
    
    -- ========================================================================
    -- PASS 3: Instance Shared Loot bucket
    -- Items with instanceID but no NPC/encounter. Previously dropped to
    -- prevent pollution (assigning all to ALL bosses). Now stored in a
    -- separate per-instance bucket and shown under "Shared Instance Loot"
    -- header when hovering any boss in that instance.
    -- Recovers 883 items (458 MoP) — mostly tier tokens, shared drops,
    -- and difficulty variants from ToT (230), HoF (102), ToES (92).
    -- ========================================================================
    
    local instanceLoot = {}      -- instanceID → { [itemID] = true }
    local npcToInstances = {}    -- npcID → { [instanceID] = true }
    
    for _, pending in ipairs(pendingInstance) do
        local iID = pending.iID
        if iID then
            -- Verify it's a transmog item
            local _, _, _, equipLoc, _, classID = GetItemInfoInstant(pending.itemID)
            if classID and owner.VALID_ITEM_CLASSES[classID] and not (equipLoc and owner.EXCLUDED_EQUIP_LOCS[equipLoc]) then
                if not instanceLoot[iID] then instanceLoot[iID] = {} end
                instanceLoot[iID][pending.itemID] = true
                stats.instanceGrouped = stats.instanceGrouped + 1
            end
        end
    end
    
    -- Build NPC → instances reverse lookup from instanceNPCs
    for iID, npcs in pairs(instanceNPCs) do
        for npc in pairs(npcs) do
            if not npcToInstances[npc] then npcToInstances[npc] = {} end
            npcToInstances[npc][iID] = true
        end
    end
    
    -- ========================================================================
    -- PASS 4: Quest NPC items (non-world-boss quest givers with transmog)
    -- These are actual quest givers where the reward is transmoggable gear.
    -- Scans QUEST items that have NPC IDs but aren't world bosses
    -- (world boss quest items were already caught in PASS 1).
    -- ========================================================================
    
    for _, expName in ipairs(expansions) do
        local data = compact[expName]
        if data and data.I then
            for itemID, raw in pairs(data.I) do
                if type(raw) == "table" then
                    local npcID = raw[2]
                    local st = raw[3]
                    if st == ST_QUEST and npcID and npcID ~= 0 and not owner.WORLD_BOSS_NPC_SET[npcID] then
                        local _, _, _, equipLoc, _, classID = GetItemInfoInstant(itemID)
                        if classID and owner.VALID_ITEM_CLASSES[classID] and not (equipLoc and owner.EXCLUDED_EQUIP_LOCS[equipLoc]) then
                            if owner.AddToNPCIndex(npcIndex, npcID, itemID, owner.FLAG_DIRECT + owner.FLAG_QUEST) then
                                stats.questNPC = stats.questNPC + 1
                            end
                        end
                    end
                end
            end
        end
    end
    
    -- ========================================================================
    -- PASS 5: Rare mob loot from RareMobLoot supplemental data
    -- 529 rare NPCs with transmog items.
    -- These are open-world rare spawns that drop BoE greens/blues.
    -- Only items that exist in our compact data are included.
    -- ========================================================================
    
    local rareMobData = NS.RareMobLoot
    if rareMobData then
        for npcID, itemList in pairs(rareMobData) do
            for _, itemID in ipairs(itemList) do
                local _, _, _, equipLoc, _, classID = GetItemInfoInstant(itemID)
                if classID and owner.VALID_ITEM_CLASSES[classID] and not (equipLoc and owner.EXCLUDED_EQUIP_LOCS[equipLoc]) then
                    if owner.AddToNPCIndex(npcIndex, npcID, itemID, owner.FLAG_DIRECT + owner.FLAG_RARE) then
                        stats.rareMob = stats.rareMob + 1
                    end
                end
            end
        end
    end
    
    -- ========================================================================
    -- PASS 6: Journal Loot Database (JLoot) supplement
    -- Uses ETB.JLoot + encToNPCs to add items from the Blizzard encounter
    -- journal that compact data missed. Maps to ALL NPCs for ALL encounters
    -- (does NOT break after first match — preserves multi-encounter items).
    -- NOTE: Does NOT use EJ_GetCreatureInfo — it returns journal section IDs
    -- in MoP 5.4.8, not actual game npcIDs.
    -- ========================================================================
    
    local jlootAdded = 0
    if ETB._BossLootLoader then ETB._BossLootLoader(); ETB._BossLootLoader = nil end
    if ETB.JLoot then
        for itemID, lootData in pairs(ETB.JLoot) do
            -- Skip tier tokens — PASS 7 handles their NPC resolution
            -- from TierTokenSource which has authoritative boss mappings.
            -- Adding them here via JLoot causes wrong-boss conflicts.
            if ETB.TierTokenMap and ETB.TierTokenMap[itemID] then
                -- skip: tier token handled by PASS 7
            else
                -- JLoot is authoritative — items are confirmed boss drops.
                -- Filter non-transmoggable slots (trinkets, rings, necks).
                local _, _, _, jEquipLoc, _, jClassID = GetItemInfoInstant(itemID)
                if jClassID and not owner.VALID_ITEM_CLASSES[jClassID] then
                    -- skip non-armor/weapon
                elseif jEquipLoc and owner.EXCLUDED_EQUIP_LOCS[jEquipLoc] then
                    -- skip trinkets, rings, necks
                else
                    local entries = type(lootData[1]) == "table" and lootData or { lootData }
                    for _, entry in ipairs(entries) do
                        local encID = entry[1]
                        local npcs = encToNPCs[encID]
                        if npcs then
                            for npc in pairs(npcs) do
                                if owner.AddToNPCIndex(npcIndex, npc, itemID, owner.FLAG_ENCOUNTER) then
                                    jlootAdded = jlootAdded + 1
                                end
                            end
                        elseif ETB.JEncToNpcs then
                            local npc = ETB.JEncToNpcs[encID]
                            if npc and npc > 0 then
                                if owner.AddToNPCIndex(npcIndex, npc, itemID, owner.FLAG_ENCOUNTER) then
                                    jlootAdded = jlootAdded + 1
                                end
                            end
                        end
                    end
                end
            end
        end
        
        ETB:Debug("BossLoot", string.format("JLoot supplement: %d new item-NPC mappings added", jlootAdded))
    end
    
    -- ========================================================================
    -- PASS 6: Inject tier tokens from TierTokenSource into NPC index
    -- Tier tokens aren't transmog items so they're not in compact data,
    -- but bosses drop them and players need to see them in tooltips.
    -- ========================================================================
    
    local tierTokensAdded = 0
    if ETB._TierTokenMapLoader then ETB._TierTokenMapLoader(); ETB._TierTokenMapLoader = nil end
    if ETB.TierTokenSource and ETB.TierTokenMap then
        for tokenID, src in pairs(ETB.TierTokenSource) do
            local encID = src[1] or 0
            local instID = src[2] or 0
            local npcID = src[3] or 0
            
            -- Resolve NPC from encounter if needed
            if npcID == 0 and encID > 0 then
                -- Check our built encToNPCs table first
                local npcs = encToNPCs[encID]
                if npcs then
                    for npc in pairs(npcs) do
                        npcID = npc
                        break
                    end
                end
                -- Fallback to JEncToNpcs
                if npcID == 0 and ETB.JEncToNpcs then
                    npcID = ETB.JEncToNpcs[encID] or 0
                end
            end
            
            if npcID > 0 then
                if owner.AddToNPCIndex(npcIndex, npcID, tokenID, owner.FLAG_DIRECT + owner.FLAG_TOKEN) then
                    tierTokensAdded = tierTokensAdded + 1
                end
            end
        end
        ETB:Debug("BossLoot", string.format("Tier tokens injected: %d", tierTokensAdded))
    end
    
    -- ========================================================================
    -- FINALIZE
    -- ========================================================================
    
    local totalNPCs, totalItems = 0, 0
    for _, items in pairs(npcIndex) do
        totalNPCs = totalNPCs + 1
        for _ in pairs(items) do
            totalItems = totalItems + 1
        end
    end
    
    owner.npcIndex = npcIndex
    owner.instanceLoot = instanceLoot        -- instanceID → { [itemID] = true }
    owner.npcToInstances = npcToInstances    -- npcID → { [instanceID] = true }
    
    -- Count instance shared loot items
    local instLootCount = 0
    for _, items in pairs(instanceLoot) do
        for _ in pairs(items) do instLootCount = instLootCount + 1 end
    end
    
    owner.stats = {
        directNPC = stats.directNPC,
        encounterResolved = stats.encounterResolved,
        instanceGrouped = stats.instanceGrouped,
        worldBoss = stats.worldBoss,
        questNPC = stats.questNPC,
        rareMob = stats.rareMob,
        jlootAdded = jlootAdded or 0,
        sdbLootAdded = sdbLootAdded or 0,
        tierTokens = tierTokensAdded or 0,
        instSharedLoot = instLootCount,
        totalNPCs = totalNPCs,
        totalItems = totalItems,
        pendingEncounter = #pendingEncounter,
        pendingInstance = #pendingInstance,
        encToNPCCount = 0,
        instanceNPCCount = 0,
    }
    for _ in pairs(encToNPCs) do owner.stats.encToNPCCount = owner.stats.encToNPCCount + 1 end
    for _ in pairs(instanceNPCs) do owner.stats.instanceNPCCount = owner.stats.instanceNPCCount + 1 end
    
    -- Build JLoot reverse index for fast hover lookups
    owner:BuildJLootIndex()
    
    -- Pre-warm item cache in staggered batches. The server throttles rapid
    -- GetItemInfo requests — batching 50 items per 0.1s avoids the throttle
    -- and ensures heroic-unique VENDOR items are cached before player hovers.
    local uncached = {}
    for _, items in pairs(npcIndex) do
        for itemID in pairs(items) do
            if not GetItemInfo(itemID) then
                uncached[#uncached + 1] = itemID
            end
        end
    end
    if #uncached > 0 then
        local BATCH_SIZE = 50
        local batchIndex = 0
        local function warmBatch()
            local startIdx = batchIndex * BATCH_SIZE + 1
            local endIdx = math.min(startIdx + BATCH_SIZE - 1, #uncached)
            if startIdx > #uncached then return end
            for i = startIdx, endIdx do
                GetItemInfo(uncached[i])
            end
            batchIndex = batchIndex + 1
            if endIdx < #uncached then
                C_Timer.After(0.1, warmBatch)
            end
        end
        warmBatch()
        ETB:Debug("BossLoot", string.format("Pre-warming cache: %d items in %d batches",
            #uncached, math.ceil(#uncached / BATCH_SIZE)))
    end
    
    owner.indexBuilt = true
    owner.building = false
    
    ETB:Debug("BossLoot", string.format(
        "Index built: %d NPCs, %d items (direct:%d, enc:%d, instShared:%d, wBoss:%d, quest:%d, rare:%d, sdbLoot:%d, tierTokens:%d)",
        totalNPCs, totalItems, stats.directNPC, stats.encounterResolved,
        instLootCount, stats.worldBoss, stats.questNPC, stats.rareMob, sdbLootAdded, tierTokensAdded or 0))
end


function Index:GetNPCIDFromGUID(owner, guid)
    if not guid then return nil end
    local unitType, _, _, _, _, npcID = strsplit("-", guid)
    if unitType == "Creature" or unitType == "Vehicle" then
        return tonumber(npcID)
    end
    return nil
end


function Index:BuildJLootIndex(owner)
    if owner._jlootByEnc then return end
    if ETB._BossLootLoader then ETB._BossLootLoader(); ETB._BossLootLoader = nil end
    if not ETB.JLoot then return end
    
    owner._jlootByEnc = {}  -- encounterID → { [itemID] = true }
    
    for itemID, lootData in pairs(ETB.JLoot) do
        local entries = type(lootData[1]) == "table" and lootData or { lootData }
        for _, entry in ipairs(entries) do
            local encID = entry[1]
            if not owner._jlootByEnc[encID] then
                owner._jlootByEnc[encID] = {}
            end
            owner._jlootByEnc[encID][itemID] = true
        end
    end
    
    local count = 0
    for _ in pairs(owner._jlootByEnc) do count = count + 1 end
    ETB:Debug("BossLoot", string.format("JLoot reverse index: %d encounters", count))
end

