local _, ns = ...

------------------------------------------------------------------------
-- EJ API Compatibility Layer
-- Blizzard moves functions between global EJ_* and C_EncounterJournal.*
------------------------------------------------------------------------
local CEJ = C_EncounterJournal or {}

local function EJ_Call(cejName, globalName, ...)
    if CEJ[cejName] then
        return CEJ[cejName](...)
    elseif _G[globalName] then
        return _G[globalName](...)
    end
    return nil
end

local function EJ_GetCurrentTierCompat()
    return EJ_Call("GetCurrentTier", "EJ_GetCurrentTier")
end
local function EJ_SelectTierCompat(tier)
    return EJ_Call("SelectTier", "EJ_SelectTier", tier)
end
local function EJ_GetNumTiersCompat()
    return EJ_Call("GetNumTiers", "EJ_GetNumTiers") or 0
end
local function EJ_GetInstanceByIndexCompat(index, isRaid)
    return EJ_Call("GetInstanceByIndex", "EJ_GetInstanceByIndex", index, isRaid)
end
local function EJ_SelectInstanceCompat(instanceID)
    return EJ_Call("SelectInstance", "EJ_SelectInstance", instanceID)
end
local function EJ_GetEncounterInfoByIndexCompat(index)
    return EJ_Call("GetEncounterInfoByIndex", "EJ_GetEncounterInfoByIndex", index)
end
local function EJ_SelectEncounterCompat(encounterID)
    return EJ_Call("SelectEncounter", "EJ_SelectEncounter", encounterID)
end
local function EJ_SetDifficultyCompat(diffID)
    return EJ_Call("SetDifficulty", "EJ_SetDifficulty", diffID)
end
local function EJ_GetDifficultyCompat()
    return EJ_Call("GetDifficulty", "EJ_GetDifficulty")
end
local function EJ_SetLootFilterCompat(classID, specID)
    return EJ_Call("SetLootFilter", "EJ_SetLootFilter", classID, specID)
end
local function EJ_SetSlotFilterCompat(slotFilter)
    return EJ_Call("SetSlotFilter", "EJ_SetSlotFilter", slotFilter)
end
local function EJ_GetNumLootCompat()
    return EJ_Call("GetNumLoot", "EJ_GetNumLoot") or 0
end
local function EJ_GetSlotFilterCompat()
    return EJ_Call("GetSlotFilter", "EJ_GetSlotFilter")
end
local function EJ_GetLootFilterCompat()
    if CEJ.GetLootFilter then
        return CEJ.GetLootFilter()
    elseif _G.EJ_GetLootFilter then
        return _G.EJ_GetLootFilter()
    end
    return nil, nil
end

local function EJ_GetLootInfoByIndexCompat(index)
    if CEJ.GetLootInfoByIndex then
        return CEJ.GetLootInfoByIndex(index)
    end
    if _G.EJ_GetLootInfoByIndex then
        local name, icon, slot, armorType, itemID, itemLink = _G.EJ_GetLootInfoByIndex(index)
        if name then
            return { name = name, icon = icon, itemID = itemID, link = itemLink }
        end
    end
    return nil
end

------------------------------------------------------------------------
-- Item Stats Compatibility
------------------------------------------------------------------------
local function GetItemStatsCompat(itemLink)
    if C_Item and C_Item.GetItemStats then
        return C_Item.GetItemStats(itemLink)
    elseif _G.GetItemStats then
        local stats = {}
        _G.GetItemStats(itemLink, stats)
        if next(stats) then return stats end
        return nil
    end
    return nil
end

------------------------------------------------------------------------
-- Constants
------------------------------------------------------------------------
local EISFT = Enum.ItemSlotFilterType

ns.LOOT_DIFF_COLORS = {
    GREEN  = { r = 0.12, g = 0.75, b = 0.12 },
    BLUE   = { r = 0.00, g = 0.44, b = 0.87 },
    PURPLE = { r = 0.64, g = 0.21, b = 0.93 },
}

ns.LOOT_DIFFICULTIES = {
    DUNGEON = {
        { id = 1,  name = "N",   fullName = "Normal",     color = "GREEN"  },
        { id = 2,  name = "H",   fullName = "Heroic",     color = "BLUE"   },
        { id = 23, name = "M",   fullName = "Mythic",     color = "PURPLE" },
    },
    RAID = {
        { id = 17, name = "LFR", fullName = "LFR",        color = "GREEN",  colOffset = 1 },
        { id = 14, name = "N",   fullName = "Normal",     color = "BLUE",   colOffset = 2 },
        { id = 15, name = "H",   fullName = "Heroic",     color = "PURPLE", colOffset = 3 },
        { id = 16, name = "M",   fullName = "Mythic",     color = "PURPLE", colOffset = 4 },
    },
    WORLD_BOSS = {
        { id = 0,  name = "WB",  fullName = "World Boss", color = "PURPLE", colOffset = 3 },
    },
}

ns.LOOT_SLOT_FILTERS = {
    { name = "Head",      filter = EISFT.Head      },
    { name = "Neck",      filter = EISFT.Neck      },
    { name = "Shoulder",  filter = EISFT.Shoulder  },
    { name = "Back",      filter = EISFT.Cloak     },
    { name = "Chest",     filter = EISFT.Chest     },
    { name = "Wrist",     filter = EISFT.Wrist     },
    { name = "Hands",     filter = EISFT.Hand      },
    { name = "Waist",     filter = EISFT.Waist     },
    { name = "Legs",      filter = EISFT.Legs      },
    { name = "Feet",      filter = EISFT.Feet      },
    { name = "Finger",    filter = EISFT.Finger    },
    { name = "Trinket",   filter = EISFT.Trinket   },
    { name = "Main Hand", filter = EISFT.MainHand  },
    { name = "Off Hand",  filter = EISFT.OffHand   },
}

ns.LOOT_SECONDARY_STATS = {
    { key = "ITEM_MOD_CRIT_RATING_SHORT",   short = "Crit",    name = "Critical Strike" },
    { key = "ITEM_MOD_HASTE_RATING_SHORT",  short = "Haste",   name = "Haste" },
    { key = "ITEM_MOD_MASTERY_RATING_SHORT", short = "Mastery", name = "Mastery" },
    { key = "ITEM_MOD_VERSATILITY",          short = "Vers",    name = "Versatility" },
}

------------------------------------------------------------------------
-- Caches
------------------------------------------------------------------------
local lootBrowserCache = {}
local instanceCache = nil
ns.isLootScanning = false

------------------------------------------------------------------------
-- Navigation State
------------------------------------------------------------------------
ns.lootBrowserState = {
    selectedClassID   = nil,
    selectedClassFile = nil,
    selectedSpecIndex = nil,  -- 1..N within selectedClassID
    selectedSpecID    = nil,
    selectedSlot      = EISFT.Head,
    selectedSlotName  = "Head",
    selectedStats     = {},
    selectedView      = "dungeon",   -- "dungeon" or "raid"
    isFavoritesMode   = false,
    isAllSlots        = false,
}

------------------------------------------------------------------------
-- EJ Interference Suppression (ref-counted)
------------------------------------------------------------------------
local ejOriginalOnEvent = nil
local ejSuppressCount = 0
local ejSavedDifficulty = nil
local ejSavedSlotFilter = nil
local ejSavedLootClassID = nil
local ejSavedLootSpecID = nil

local function SuppressEJ()
    ejSuppressCount = ejSuppressCount + 1
    if ejSuppressCount > 1 then return end

    if EncounterJournal then
        ejSavedDifficulty = EJ_GetDifficultyCompat()
        ejSavedSlotFilter = EJ_GetSlotFilterCompat()
        ejSavedLootClassID, ejSavedLootSpecID = EJ_GetLootFilterCompat()

        ejOriginalOnEvent = EncounterJournal:GetScript("OnEvent")
        EncounterJournal:SetScript("OnEvent", nil)
        EncounterJournal:UnregisterEvent("EJ_LOOT_DATA_RECIEVED")
        EncounterJournal:UnregisterEvent("EJ_DIFFICULTY_UPDATE")
        EncounterJournal:UnregisterEvent("UNIT_LEVEL")
    end
end

local function UnsuppressEJ()
    ejSuppressCount = ejSuppressCount - 1
    if ejSuppressCount > 0 then return end
    ejSuppressCount = 0

    if EncounterJournal then
        if ejSavedDifficulty then
            EJ_SetDifficultyCompat(ejSavedDifficulty)
            ejSavedDifficulty = nil
        end
        if ejSavedSlotFilter then
            EJ_SetSlotFilterCompat(ejSavedSlotFilter)
            ejSavedSlotFilter = nil
        end
        if ejSavedLootClassID and ejSavedLootSpecID then
            EJ_SetLootFilterCompat(ejSavedLootClassID, ejSavedLootSpecID)
            ejSavedLootClassID = nil
            ejSavedLootSpecID = nil
        end

        if ejOriginalOnEvent then
            EncounterJournal:SetScript("OnEvent", ejOriginalOnEvent)
            ejOriginalOnEvent = nil
        end
        EncounterJournal:RegisterEvent("EJ_LOOT_DATA_RECIEVED")
        EncounterJournal:RegisterEvent("EJ_DIFFICULTY_UPDATE")
        EncounterJournal:RegisterEvent("UNIT_LEVEL")
    end
end

------------------------------------------------------------------------
-- Seasonal Dungeon Allowlist (Midnight Season 1)
------------------------------------------------------------------------
local SEASONAL_DUNGEON_IDS = {
    [1300] = true,  -- Magisters' Terrace
    [1315] = true,  -- Maisara Caverns
    [1316] = true,  -- Nexus-Point Xenas
    [1299] = true,  -- Windrunner Spire
    [1201] = true,  -- Algeth'ar Academy
    [278]  = true,  -- Pit of Saron
    [945]  = true,  -- Seat of the Triumvirate
    [476]  = true,  -- Skyreach
}

-- Background texture IDs for dungeon rows (from WoW's internal textures)
local DUNGEON_BG_TEXTURES = {
    [476]  = 1041999,  -- Skyreach
    [945]  = 1718213,  -- Seat of the Triumvirate
    [1201] = 4742929,  -- Algeth'ar Academy
    [278]  = 608210,   -- Pit of Saron
    [1299] = 7464937,  -- Windrunner Spire
    [1300] = 7467174,  -- Magisters' Terrace
    [1316] = 7570501,  -- Nexus-Point Xenas
    [1315] = 7478529,  -- Maisara Caverns
}

local WORLD_BOSS_INSTANCE_ID = 1312

------------------------------------------------------------------------
-- Instance + Boss Discovery (cached per session)
------------------------------------------------------------------------
local function BuildInstanceCache()
    if instanceCache then return instanceCache end

    SuppressEJ()

    local ok, result = pcall(function()
        local instances = { dungeons = {}, raids = {}, worldBosses = {} }

        local currentTier = EJ_GetCurrentTierCompat()
        if not currentTier then return nil end

        local numTiers = EJ_GetNumTiersCompat()
        local foundDungeons = {}
        for tier = numTiers, 1, -1 do
            EJ_SelectTierCompat(tier)
            local index = 1
            while true do
                local instanceID, name = EJ_GetInstanceByIndexCompat(index, false)
                if not instanceID then break end
                if SEASONAL_DUNGEON_IDS[instanceID] and not foundDungeons[instanceID] then
                    foundDungeons[instanceID] = true
                    EJ_SelectInstanceCompat(instanceID)
                    local bosses = {}
                    local bi = 1
                    while true do
                        local bossName, _, bossID = EJ_GetEncounterInfoByIndexCompat(bi)
                        if not bossName then break end
                        table.insert(bosses, { name = bossName, encounterID = bossID })
                        bi = bi + 1
                    end

                    local hasNormal = false
                    if #bosses > 0 then
                        EJ_SelectEncounterCompat(bosses[1].encounterID)
                        EJ_SetDifficultyCompat(1)
                        EJ_SetSlotFilterCompat(EISFT.NoFilter)
                        local numLoot = EJ_GetNumLootCompat()
                        if numLoot and numLoot > 0 then
                            local testInfo = EJ_GetLootInfoByIndexCompat(1)
                            if testInfo and testInfo.link then
                                local _, _, _, itemLevel = GetItemInfo(testInfo.link)
                                if itemLevel then
                                    hasNormal = (itemLevel >= 200)
                                end
                            end
                        end
                    end

                    table.insert(instances.dungeons, {
                        instanceID = instanceID,
                        name = name,
                        bosses = bosses,
                        hasNormal = hasNormal,
                        bgTexture = DUNGEON_BG_TEXTURES[instanceID],
                    })
                    EJ_SelectTierCompat(tier)
                end
                index = index + 1
            end
        end

        EJ_SelectTierCompat(currentTier)

        local raidIdx = 1
        while true do
            local instanceID, name = EJ_GetInstanceByIndexCompat(raidIdx, true)
            if not instanceID then break end

            EJ_SelectInstanceCompat(instanceID)
            local bosses = {}
            local bi = 1
            while true do
                local bossName, _, bossID = EJ_GetEncounterInfoByIndexCompat(bi)
                if not bossName then break end
                table.insert(bosses, { name = bossName, encounterID = bossID })
                bi = bi + 1
            end

            if instanceID == WORLD_BOSS_INSTANCE_ID then
                table.insert(instances.worldBosses, {
                    instanceID = instanceID,
                    name = name,
                    bosses = bosses,
                })
            else
                table.insert(instances.raids, {
                    instanceID = instanceID,
                    name = name,
                    bosses = bosses,
                })
            end
            raidIdx = raidIdx + 1
        end

        return instances
    end)

    UnsuppressEJ()

    if not ok then
        print("|cff00ff00YippYapp|r |cffff6060Loot Browser:|r Instance cache error: " .. tostring(result))
        return nil
    end

    instanceCache = result
    return result
end

function ns:GetInstanceCache()
    if not instanceCache then
        BuildInstanceCache()
    end
    return instanceCache
end

------------------------------------------------------------------------
-- Loot Scanning
------------------------------------------------------------------------
local function ScanLootForEncounter(instanceID, encounterID, difficultyID, classID, specID, slotFilter)
    local items = {}
    EJ_SelectInstanceCompat(instanceID)
    EJ_SelectEncounterCompat(encounterID)
    EJ_SetDifficultyCompat(difficultyID)

    if classID and specID then
        EJ_SetLootFilterCompat(classID, specID)
    end
    if slotFilter then
        EJ_SetSlotFilterCompat(slotFilter)
    end

    local index = 1
    while true do
        local info = EJ_GetLootInfoByIndexCompat(index)
        if not info or not info.name then break end
        table.insert(items, {
            name     = info.name,
            icon     = info.icon,
            itemID   = info.itemID,
            itemLink = info.link,
        })
        index = index + 1
    end
    return items
end

local function ScanSourceType(results, instances, sourceType, difficulties, classID, specID, slotFilter)
    for _, inst in ipairs(instances) do
        for _, boss in ipairs(inst.bosses) do
            local entry = {
                sourceName  = inst.name,
                sourceType  = sourceType,
                bossName    = boss.name,
                encounterID = boss.encounterID,
                items       = {},
            }

            if sourceType == "worldboss" then
                for _, tryDiff in ipairs({0, 14, 15}) do
                    local items = ScanLootForEncounter(inst.instanceID, boss.encounterID, tryDiff, classID, specID, slotFilter)
                    if #items > 0 then
                        entry.items[0] = items
                        break
                    end
                end
            else
                for _, diff in ipairs(difficulties) do
                    local skip = false
                    if sourceType == "dungeon" and diff.id == 1 and inst.hasNormal == false then
                        skip = true
                    end
                    if not skip then
                        local items = ScanLootForEncounter(inst.instanceID, boss.encounterID, diff.id, classID, specID, slotFilter)
                        if #items > 0 then
                            entry.items[diff.id] = items
                        end
                    end
                end
            end

            local hasItems = false
            for _ in pairs(entry.items) do hasItems = true; break end
            if hasItems then
                table.insert(results, entry)
            end
        end
    end
end

function ns:ScanLootBrowserSlot(specIndex, slotFilter)
    local classID = ns.lootBrowserState.selectedClassID or select(3, UnitClass("player"))
    local specID = ns.lootBrowserState.selectedSpecID
        or GetSpecializationInfoForClassID(classID, specIndex)

    local cacheKey = specID .. "-" .. slotFilter
    if lootBrowserCache[cacheKey] then
        return lootBrowserCache[cacheKey]
    end

    if not instanceCache then
        local built = BuildInstanceCache()
        if not built then
            print("|cff00ff00YippYapp|r |cffff6060Loot Browser:|r Could not load instance data. Try /reload.")
            return {}
        end
    end

    SuppressEJ()
    ns.isLootScanning = true

    local ok, results = pcall(function()
        local r = {}
        ScanSourceType(r, instanceCache.dungeons,    "dungeon",   ns.LOOT_DIFFICULTIES.DUNGEON,    classID, specID, slotFilter)
        ScanSourceType(r, instanceCache.raids,       "raid",      ns.LOOT_DIFFICULTIES.RAID,       classID, specID, slotFilter)
        ScanSourceType(r, instanceCache.worldBosses, "worldboss", ns.LOOT_DIFFICULTIES.WORLD_BOSS, classID, specID, slotFilter)
        return r
    end)

    -- Populate the cache BEFORE unsuppressing. UnsuppressEJ restores
    -- difficulty/slot/loot filters, which fires EJ_DIFFICULTY_UPDATE /
    -- EJ_LOOT_DATA_RECIEVED; our ejFrame handler wipes the cache on those
    -- events unless isLootScanning is still true. Keep the flag set across
    -- the unsuppress so the restore events are ignored.
    if ok then
        lootBrowserCache[cacheKey] = results
    end

    UnsuppressEJ()
    ns.isLootScanning = false

    if not ok then
        print("|cff00ff00YippYapp|r |cffff6060Loot Browser:|r Scan error: " .. tostring(results))
        return {}
    end

    return results
end

------------------------------------------------------------------------
-- Secondary Stat Filtering (client-side post-processing)
------------------------------------------------------------------------
function ns:FilterLootByStats(results, statKeys)
    if not statKeys or #statKeys == 0 then return results, 0 end

    local preFilterCount = #results
    local filtered = {}
    for _, entry in ipairs(results) do
        local newEntry = {
            sourceName  = entry.sourceName,
            sourceType  = entry.sourceType,
            bossName    = entry.bossName,
            encounterID = entry.encounterID,
            items       = {},
        }

        for diffID, items in pairs(entry.items) do
            local kept = {}
            for _, item in ipairs(items) do
                if not item.itemLink then
                    table.insert(kept, item)
                else
                    local stats = GetItemStatsCompat(item.itemLink)
                    if not stats then
                        table.insert(kept, item)
                    else
                        local hasAll = true
                        for _, key in ipairs(statKeys) do
                            if not stats[key] then
                                hasAll = false
                                break
                            end
                        end
                        if hasAll then
                            table.insert(kept, item)
                        end
                    end
                end
            end
            if #kept > 0 then
                newEntry.items[diffID] = kept
            end
        end

        local hasItems = false
        for _ in pairs(newEntry.items) do hasItems = true; break end
        if hasItems then
            table.insert(filtered, newEntry)
        end
    end
    return filtered, preFilterCount
end

-- Filter already-shaped data (Favorites / All Slots paths) whose items are
-- a single flat list per instance rather than per-difficulty dicts.
function ns:FilterShapedLootByStats(shaped, statKeys)
    if not statKeys or #statKeys == 0 then return shaped end
    local out = {}
    for _, inst in ipairs(shaped) do
        local kept = {}
        for _, item in ipairs(inst.items) do
            local keep = true
            if item.itemLink then
                local stats = GetItemStatsCompat(item.itemLink)
                if stats then
                    for _, key in ipairs(statKeys) do
                        if not stats[key] then keep = false; break end
                    end
                end
            end
            if keep then table.insert(kept, item) end
        end
        if #kept > 0 then
            table.insert(out, {
                instanceName = inst.instanceName,
                sourceType   = inst.sourceType,
                bgTexture    = inst.bgTexture,
                items        = kept,
            })
        end
    end
    return out
end

------------------------------------------------------------------------
-- Cache Invalidation
------------------------------------------------------------------------
function ns:ClearLootBrowserCache()
    wipe(lootBrowserCache)
end

function ns:ClearAllLootBrowserCaches()
    wipe(lootBrowserCache)
    instanceCache = nil
end

------------------------------------------------------------------------
-- Instance-aware loot: detect current dungeon/raid and filter results
------------------------------------------------------------------------
function ns:GetCurrentInstanceName()
    local inInstance, instanceType = IsInInstance()
    if not inInstance or (instanceType ~= "party" and instanceType ~= "raid") then
        return nil, nil
    end
    local name = GetInstanceInfo()
    return name, instanceType
end

function ns:FilterLootByInstance(results, instanceName)
    if not instanceName or not results then return results end

    local lowerInst = strlower(instanceName)
    local filtered = {}
    for _, entry in ipairs(results) do
        if entry.sourceName == instanceName or strlower(entry.sourceName) == lowerInst then
            table.insert(filtered, entry)
        end
    end
    return filtered
end



------------------------------------------------------------------------
-- Navigation Functions
------------------------------------------------------------------------
function ns:LootBrowser_DetectSpec()
    local _, classFile, classID = UnitClass("player")
    ns.lootBrowserState.selectedClassID = classID
    ns.lootBrowserState.selectedClassFile = classFile

    local lootSpecID = GetLootSpecialization()
    if lootSpecID == 0 then
        local currentSpec = GetSpecialization()
        if currentSpec then
            ns.lootBrowserState.selectedSpecIndex = currentSpec
            ns.lootBrowserState.selectedSpecID = select(1, GetSpecializationInfo(currentSpec))
        end
    else
        for i = 1, GetNumSpecializations() do
            local specID = GetSpecializationInfo(i)
            if specID == lootSpecID then
                ns.lootBrowserState.selectedSpecIndex = i
                ns.lootBrowserState.selectedSpecID = specID
                break
            end
        end
    end

    -- Default to Favorites if any exist, otherwise All Slots
    if ns:GetFavoritesCount() > 0 then
        ns.lootBrowserState.isFavoritesMode = true
        ns.lootBrowserState.isAllSlots = false
        ns.lootBrowserState.selectedSlot = nil
        ns.lootBrowserState.selectedSlotName = "Favorites"
    else
        ns.lootBrowserState.isFavoritesMode = false
        ns.lootBrowserState.isAllSlots = true
        ns.lootBrowserState.selectedSlot = nil
        ns.lootBrowserState.selectedSlotName = "All Slots"
    end
end

function ns:LootBrowser_SwitchSpec(specIndex)
    if ns.isLootScanning then return end
    local classID = ns.lootBrowserState.selectedClassID
    if not classID then return end
    local specID = GetSpecializationInfoForClassID(classID, specIndex)
    if not specID then return end
    ns.lootBrowserState.selectedSpecIndex = specIndex
    ns.lootBrowserState.selectedSpecID = specID
    ns:ClearLootBrowserCache()
    if ns.LootBrowser_ShowPage then ns:LootBrowser_ShowPage() end
end

function ns:LootBrowser_SwitchClass(classID)
    if ns.isLootScanning then return end
    if not classID then return end
    local _, classFile = GetClassInfo(classID)
    ns.lootBrowserState.selectedClassID = classID
    ns.lootBrowserState.selectedClassFile = classFile
    local specID = GetSpecializationInfoForClassID(classID, 1)
    ns.lootBrowserState.selectedSpecIndex = 1
    ns.lootBrowserState.selectedSpecID = specID
    ns:ClearLootBrowserCache()
    if ns.LootBrowser_UpdateSpecBar then ns:LootBrowser_UpdateSpecBar() end
    if ns.LootBrowser_ShowPage then ns:LootBrowser_ShowPage() end
end

function ns:LootBrowser_SwitchSlot(slotFilter, slotName)
    if slotFilter == "FAVORITES" then
        ns.lootBrowserState.isFavoritesMode = true
        ns.lootBrowserState.isAllSlots = false
        ns.lootBrowserState.selectedSlot = nil
        ns.lootBrowserState.selectedSlotName = "Favorites"
    elseif slotFilter == "ALL_SLOTS" then
        ns.lootBrowserState.isFavoritesMode = false
        ns.lootBrowserState.isAllSlots = true
        ns.lootBrowserState.selectedSlot = nil
        ns.lootBrowserState.selectedSlotName = "All Slots"
    else
        ns.lootBrowserState.isFavoritesMode = false
        ns.lootBrowserState.isAllSlots = false
        ns.lootBrowserState.selectedSlot = slotFilter
        ns.lootBrowserState.selectedSlotName = slotName
    end
    if ns.LootBrowser_ShowPage then ns:LootBrowser_ShowPage() end
end

function ns:LootBrowser_SwitchView(view)
    ns.lootBrowserState.selectedView = view
    if ns.LootBrowser_UpdateViewTabs then ns:LootBrowser_UpdateViewTabs() end
    if ns.LootBrowser_RefreshDisplay then ns:LootBrowser_RefreshDisplay() end
end

function ns:LootBrowser_ToggleStat(statKey)
    local stats = ns.lootBrowserState.selectedStats
    for i, key in ipairs(stats) do
        if key == statKey then
            table.remove(stats, i)
            ns:LootBrowser_OnStatFilterChanged()
            return
        end
    end
    if #stats >= 2 then return end
    table.insert(stats, statKey)
    ns:LootBrowser_OnStatFilterChanged()
end

function ns:LootBrowser_ClearStats()
    wipe(ns.lootBrowserState.selectedStats)
    ns:LootBrowser_OnStatFilterChanged()
end

function ns:LootBrowser_OnStatFilterChanged()
    if ns.LootBrowser_UpdateStatBar then ns:LootBrowser_UpdateStatBar() end
    if ns.lootBrowserState.selectedSlot or ns.lootBrowserState.isFavoritesMode or ns.lootBrowserState.isAllSlots then
        if ns.LootBrowser_RefreshDisplay then
            ns:LootBrowser_RefreshDisplay()
        end
    end
end

------------------------------------------------------------------------
-- Favorites System
------------------------------------------------------------------------
function ns:GetLootCharacterKey()
    return UnitName("player") .. "-" .. GetRealmName()
end

local function GetFavoritesTable(create)
    YippYappHelperDB = YippYappHelperDB or {}
    if not YippYappHelperDB.lootFavorites then
        if not create then return nil end
        YippYappHelperDB.lootFavorites = {}
    end
    local charKey = ns:GetLootCharacterKey()
    if not YippYappHelperDB.lootFavorites[charKey] then
        if not create then return nil end
        YippYappHelperDB.lootFavorites[charKey] = {}
    end
    local specID = ns.lootBrowserState.selectedSpecID
    if not specID then return nil end
    if not YippYappHelperDB.lootFavorites[charKey][specID] then
        if not create then return nil end
        YippYappHelperDB.lootFavorites[charKey][specID] = {}
    end
    return YippYappHelperDB.lootFavorites[charKey][specID]
end

function ns:IsLootFavorite(itemID)
    local tbl = GetFavoritesTable(false)
    return tbl and tbl[itemID] or false
end

function ns:ToggleLootFavorite(itemID)
    if ns:IsLootFavorite(itemID) then
        local tbl = GetFavoritesTable(false)
        if tbl then tbl[itemID] = nil end
    else
        local tbl = GetFavoritesTable(true)
        if tbl then tbl[itemID] = true end
    end
end

function ns:GetAllFavoriteItemIDs()
    local tbl = GetFavoritesTable(false)
    return tbl or {}
end

function ns:GetFavoritesCount()
    local tbl = GetFavoritesTable(false)
    if not tbl then return 0 end
    local count = 0
    for _ in pairs(tbl) do count = count + 1 end
    return count
end

------------------------------------------------------------------------
-- Reshape: regroup per-boss results into per-instance with dedup
------------------------------------------------------------------------
-- Difficulty priority for dedup (higher = preferred for display)
local DIFF_PRIORITY = {
    [23] = 6,  -- Mythic dungeon
    [2]  = 5,  -- Heroic dungeon
    [1]  = 4,  -- Normal dungeon
    [16] = 6,  -- Mythic raid
    [15] = 5,  -- Heroic raid
    [14] = 4,  -- Normal raid
    [17] = 3,  -- LFR
    [0]  = 4,  -- World boss
}

function ns:ReshapeLootByInstance(results)
    -- Build bgTexture lookup from instance cache
    local bgLookup = {}
    local cache = instanceCache
    if cache then
        for _, list in ipairs({ cache.dungeons, cache.raids, cache.worldBosses }) do
            for _, inst in ipairs(list) do
                bgLookup[inst.name] = inst.bgTexture
            end
        end
    end

    local byInstance = {}   -- instanceName -> { sourceType, items[] }
    local instanceOrder = {} -- preserve encounter order

    for _, entry in ipairs(results) do
        local instName = entry.sourceName
        if not byInstance[instName] then
            byInstance[instName] = {
                instanceName = instName,
                sourceType   = entry.sourceType,
                bgTexture    = bgLookup[instName],
                items        = {},
                itemSeen     = {},  -- itemID -> index in items
            }
            table.insert(instanceOrder, instName)
        end
        local inst = byInstance[instName]

        -- Collect items from all difficulties, keeping highest-priority version
        for diffID, diffItems in pairs(entry.items) do
            local priority = DIFF_PRIORITY[diffID] or 0
            for _, item in ipairs(diffItems) do
                local existIdx = inst.itemSeen[item.itemID]
                if existIdx then
                    local existing = inst.items[existIdx]
                    if priority > (existing._priority or 0) then
                        existing.itemLink = item.itemLink
                        existing.icon = item.icon
                        existing.diffID = diffID
                        existing._priority = priority
                        existing.bossName = entry.bossName
                    end
                else
                    local newItem = {
                        itemID   = item.itemID,
                        name     = item.name,
                        icon     = item.icon,
                        itemLink = item.itemLink,
                        diffID   = diffID,
                        bossName = entry.bossName,
                        _priority = priority,
                    }
                    table.insert(inst.items, newItem)
                    inst.itemSeen[item.itemID] = #inst.items
                end
            end
        end
    end

    -- Build ordered result, strip internal bookkeeping
    local shaped = {}
    for _, instName in ipairs(instanceOrder) do
        local inst = byInstance[instName]
        inst.itemSeen = nil
        for _, item in ipairs(inst.items) do
            item._priority = nil
        end
        table.insert(shaped, inst)
    end
    return shaped
end

------------------------------------------------------------------------
-- Favorites Loot Scan: scan all slots, merge, filter to favorites
------------------------------------------------------------------------
function ns:ScanFavoritesLoot()
    local specIndex = ns.lootBrowserState.selectedSpecIndex
    if not specIndex then return {} end

    local favIDs = ns:GetAllFavoriteItemIDs()
    local hasFavs = false
    for _ in pairs(favIDs) do hasFavs = true; break end
    if not hasFavs then return {} end

    -- One scan with NoFilter returns every slot at once — ~14x faster than
    -- scanning each slot filter individually.
    local allResults = ns:ScanLootBrowserSlot(specIndex, EISFT.NoFilter) or {}

    -- Reshape by instance
    local shaped = ns:ReshapeLootByInstance(allResults)

    -- Filter to only favorited items
    local filtered = {}
    for _, inst in ipairs(shaped) do
        local kept = {}
        for _, item in ipairs(inst.items) do
            if favIDs[item.itemID] then
                table.insert(kept, item)
            end
        end
        if #kept > 0 then
            table.insert(filtered, {
                instanceName = inst.instanceName,
                sourceType   = inst.sourceType,
                items        = kept,
            })
        end
    end

    -- Sort by number of favorites descending
    table.sort(filtered, function(a, b)
        return #a.items > #b.items
    end)

    return filtered
end

------------------------------------------------------------------------
-- All Slots Scan: scan all 14 slots and merge by instance
------------------------------------------------------------------------
function ns:ScanAllSlotsLoot()
    local specIndex = ns.lootBrowserState.selectedSpecIndex
    if not specIndex then return {} end

    -- One scan with NoFilter returns every slot at once — ~14x faster than
    -- scanning each slot filter individually.
    local allResults = ns:ScanLootBrowserSlot(specIndex, EISFT.NoFilter) or {}
    return ns:ReshapeLootByInstance(allResults)
end

------------------------------------------------------------------------
-- EJ Event Listener (debounced cache refresh)
------------------------------------------------------------------------
local ejFrame = CreateFrame("Frame")
local lbRefreshPending = false
ejFrame:RegisterEvent("EJ_LOOT_DATA_RECIEVED")
ejFrame:RegisterEvent("EJ_DIFFICULTY_UPDATE")
ejFrame:SetScript("OnEvent", function()
    if not ns.isLootScanning then
        -- Only invalidate + refresh when the panel is open; keep the cache
        -- warm otherwise so reopening feels instant.
        if ns.LootBrowserFrame and ns.LootBrowserFrame:IsShown() then
            ns:ClearLootBrowserCache()
            if not lbRefreshPending
               and (ns.lootBrowserState.selectedSlot or ns.lootBrowserState.isFavoritesMode or ns.lootBrowserState.isAllSlots)
               and ns.LootBrowser_RefreshDisplay then
                lbRefreshPending = true
                C_Timer.After(0.5, function()
                    lbRefreshPending = false
                    if ns.LootBrowserFrame and ns.LootBrowserFrame:IsShown()
                       and (ns.lootBrowserState.selectedSlot or ns.lootBrowserState.isFavoritesMode or ns.lootBrowserState.isAllSlots) then
                        ns:LootBrowser_RefreshDisplay()
                    end
                end)
            end
        end
    end
end)

------------------------------------------------------------------------
-- Prewarm: background-scan common filters shortly after login so the
-- first panel open doesn't pay the EJ async-fetch latency.
------------------------------------------------------------------------
local EISFT = Enum.ItemSlotFilterType
local prewarmFrame = CreateFrame("Frame")
prewarmFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
prewarmFrame:SetScript("OnEvent", function(self)
    self:UnregisterEvent("PLAYER_ENTERING_WORLD")
    -- First pass at 5s: EJ has usually populated its initial map by then.
    C_Timer.After(5, function()
        if ns.LootBrowser_DetectSpec then ns:LootBrowser_DetectSpec() end
        local state = ns.lootBrowserState
        if not state.selectedSpecIndex then return end
        -- NoFilter returns every slot in one scan → powers All Slots/Favorites
        -- and is the longest single scan; running it primes the EJ cache for
        -- the per-slot scans that follow.
        ns:ScanLootBrowserSlot(state.selectedSpecIndex, EISFT.NoFilter)
    end)
    -- Second pass at 12s to cover slot-specific cache for the default "Head".
    C_Timer.After(12, function()
        local state = ns.lootBrowserState
        if not state.selectedSpecIndex then return end
        ns:ScanLootBrowserSlot(state.selectedSpecIndex, EISFT.Head)
    end)
end)
