--------------------------------------------------------------
-- LifaMpLoop  Core.lua  –  Addon bootstrap & saved variables 
-- ## Author: Lifa ## Version: 1.1.4 ## Notes-zhCN: 大秘境装备掉落浏览与关注列表 ## SavedVariablesPerCharacter: LifaMpLoopDB
--------------------------------------------------------------
local LPL = {}

if GetLocale() == "zhCN" or GetLocale() == "zhTW" then
    LPL["ADDON_NAME"] = "力法的大米掉落小本本"
    LPL["SLASH_HELP"] = "输入 /mpl 打开大秘境掉落浏览器"
    LPL["TAB_SLOT"] = "部位"
    LPL["TAB_STATS"] = "属性"
    LPL["TAB_DUNGEON"] = "地下城"
    LPL["TAB_WATCHLIST"] = "关注列表"
    LPL["SLOT_HEAD"] = "头部"
    LPL["SLOT_NECK"] = "颈部"
    LPL["SLOT_SHOULDER"] = "肩部"
    LPL["SLOT_BACK"] = "背部"
    LPL["SLOT_CHEST"] = "胸部"
    LPL["SLOT_WRIST"] = "腕部"
    LPL["SLOT_HANDS"] = "手部"
    LPL["SLOT_WAIST"] = "腰部"
    LPL["SLOT_LEGS"] = "腿部"
    LPL["SLOT_FEET"] = "脚部"
    LPL["SLOT_FINGER"] = "戒指"
    LPL["SLOT_TRINKET"] = "饰品"
    LPL["SLOT_WEAPON"] = "武器"
    LPL["SLOT_OFFHAND"] = "副手"
    LPL["STAT_CRIT"] = "暴击"
    LPL["STAT_HASTE"] = "急速"
    LPL["STAT_MASTERY"] = "精通"
    LPL["STAT_VERSATILITY"] = "全能"
    LPL["ADD_TO_WATCHLIST"] = "加入关注列表"
    LPL["REMOVE_FROM_WATCHLIST"] = "从关注列表移除"
    LPL["WATCHLIST_EMPTY"] = "关注列表为空，右键点击装备将其加入关注列表"
    LPL["WATCHLIST_DUNGEON_HINT"] = "需要刷以下地下城:"
    LPL["LOOT_WHISPER_MSG"] = "你好!这件神仙装备 %s ，如果你不要,能否惠赠于我呢?非常感谢!!!"
    LPL["ITEM_OBTAINED"] = "恭喜！你获取了关注的装备 %s，已自动从关注列表移除。"
    LPL["WHISPER_SENT"] = "已自动向 %s 发送了装备请求密语。"
    LPL["LOADING_DATA"] = "正在加载数据..."
    LPL["NO_RESULTS"] = "没有找到匹配的装备"
    LPL["STAT_PRIORITY"] = "属性优先级 (左键前移/右键后移):"
    LPL["FILTER_STATS"] = "筛选属性:"
    LPL["SEARCH"] = "搜索..."
    LPL["FROM_DUNGEON"] = "来自: %s"
    LPL["FROM_BOSS"] = "掉落自: %s"
    LPL["ILVL"] = "装等: %d"
    LPL["WATCHING"] = "[关注中]"
    LPL["CLICK_TO_WATCH"] = "右键点击加入关注"
    LPL["MINIMAP_TOOLTIP"] = "左键打开大秘境掉落浏览器\n右键打开关注列表"
    LPL["ALL_SLOTS"] = "全部部位"
    LPL["FILTER_SLOT"] = "部位:"
    LPL["ALL_SPECS"] = "全部专精"
    LPL["AUTO_WHISPER"] = "自动密语队友请求装备"
    LPL["WHISPER_TEMPLATE_LABEL"] = "密语内容 (用 %s 代表物品名):"
    LPL["CHAR_FRAME_TAB"] = "大秘境掉落"
    LPL["KEYSTONE_LEVEL"] = "钥石层数: %d"
    LPL["DUNGEON_DROP_ILVL"] = "掉落装等: %d"
    LPL["VAULT_ILVL"] = "宝库装等: %d"
else
    LPL["ADDON_NAME"] = "Lifa M+ Loop"
    LPL["SLASH_HELP"] = "Type /mpl to open M+ Loot Browser"
    LPL["TAB_SLOT"] = "Slot"
    LPL["TAB_STATS"] = "Stats"
    LPL["TAB_DUNGEON"] = "Dungeon"
    LPL["TAB_WATCHLIST"] = "Watchlist"
    LPL["SLOT_HEAD"] = "Head"
    LPL["SLOT_NECK"] = "Neck"
    LPL["SLOT_SHOULDER"] = "Shoulder"
    LPL["SLOT_BACK"] = "Back"
    LPL["SLOT_CHEST"] = "Chest"
    LPL["SLOT_WRIST"] = "Wrist"
    LPL["SLOT_HANDS"] = "Hands"
    LPL["SLOT_WAIST"] = "Waist"
    LPL["SLOT_LEGS"] = "Legs"
    LPL["SLOT_FEET"] = "Feet"
    LPL["SLOT_FINGER"] = "Finger"
    LPL["SLOT_TRINKET"] = "Trinket"
    LPL["SLOT_WEAPON"] = "Weapon"
    LPL["SLOT_OFFHAND"] = "Off Hand"
    LPL["STAT_CRIT"] = "Crit"
    LPL["STAT_HASTE"] = "Haste"
    LPL["STAT_MASTERY"] = "Mastery"
    LPL["STAT_VERSATILITY"] = "Versatility"
    LPL["ADD_TO_WATCHLIST"] = "Add to Watchlist"
    LPL["REMOVE_FROM_WATCHLIST"] = "Remove from Watchlist"
    LPL["WATCHLIST_EMPTY"] = "Watchlist is empty. Right-click items to add."
    LPL["WATCHLIST_DUNGEON_HINT"] = "Dungeons to run:"
    LPL["LOOT_WHISPER_MSG"] = "Hi! Do you need the %s that just dropped? If not, would you mind trading it to me? Thanks a lot!"
    LPL["ITEM_OBTAINED"] = "Grats! You obtained watched item %s, removed from watchlist."
    LPL["WHISPER_SENT"] = "Auto-whispered %s for the item."
    LPL["LOADING_DATA"] = "Loading data..."
    LPL["NO_RESULTS"] = "No matching items found"
    LPL["STAT_PRIORITY"] = "Stat Priority (Left=Up / Right=Down):"
    LPL["FILTER_STATS"] = "Filter Stats:"
    LPL["SEARCH"] = "Search..."
    LPL["FROM_DUNGEON"] = "From: %s"
    LPL["FROM_BOSS"] = "Drops from: %s"
    LPL["ILVL"] = "iLvl: %d"
    LPL["WATCHING"] = "[Watching]"
    LPL["CLICK_TO_WATCH"] = "Right-click to watch"
    LPL["MINIMAP_TOOLTIP"] = "Left-click: M+ Loot Browser\nRight-click: Watchlist"
    LPL["ALL_SLOTS"] = "All Slots"
    LPL["FILTER_SLOT"] = "Slot:"
    LPL["ALL_SPECS"] = "All Specs"
    LPL["AUTO_WHISPER"] = "Auto-whisper teammates for loot"
    LPL["WHISPER_TEMPLATE_LABEL"] = "Whisper message (use %s for item name):"
    LPL["CHAR_FRAME_TAB"] = "M+ Loot"
    LPL["KEYSTONE_LEVEL"] = "Key Level: %d"
    LPL["DUNGEON_DROP_ILVL"] = "Drop iLvl: %d"
    LPL["VAULT_ILVL"] = "Vault iLvl: %d"
end


local LifaMpLoop = {}

LifaMpLoop.version = "1.1.4"
LifaMpLoop.loaded = false

-- Saved variable defaults
local DB_DEFAULTS = {
    watchlist = {},          -- { [itemID] = { itemID, dungeonName, bossName, addedTime } }
    statPriority = {         -- 默认属性优先级
        "ITEM_MOD_HASTE_RATING_SHORT",
        "ITEM_MOD_CRIT_RATING_SHORT",
        "ITEM_MOD_MASTERY_RATING_SHORT",
        "ITEM_MOD_VERSATILITY",
    },
    autoWhisper = true,      -- 自动密语队友请求装备
    whisperTemplate = "",     -- 自定义密语内容，空则使用默认
    lootCache = {},           -- { ["classID-specID"] = { items={...}, time=timestamp } }
    minimap = { hide = false },
}

--------------------------------------------------------------
-- Event handling
--------------------------------------------------------------
local frame = CreateFrame("Frame")
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_LOGIN")

frame:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" and arg1 == "OrzUI" then
        -- Init saved variables
        if not LifaMpLoopDB then
            LifaMpLoopDB = CopyTable(DB_DEFAULTS)
        else
            -- merge defaults for newly added keys
            for k, v in pairs(DB_DEFAULTS) do
                if LifaMpLoopDB[k] == nil then
                    LifaMpLoopDB[k] = type(v) == "table" and CopyTable(v) or v
                end
            end
        end
        LifaMpLoop.db = LifaMpLoopDB

        self:UnregisterEvent("ADDON_LOADED")

    elseif event == "PLAYER_LOGIN" then
        -- Ensure db is initialized (fallback if ADDON_LOADED didn't match)
        if not LifaMpLoop.db then
            if not LifaMpLoopDB then
                LifaMpLoopDB = CopyTable(DB_DEFAULTS)
            else
                for k, v in pairs(DB_DEFAULTS) do
                    if LifaMpLoopDB[k] == nil then
                        LifaMpLoopDB[k] = type(v) == "table" and CopyTable(v) or v
                    end
                end
            end
            LifaMpLoop.db = LifaMpLoopDB
        end

        -- Default: current class + spec
        local classID = select(3, UnitClass("player"))
        local specIndex = GetSpecialization()
        local specID = specIndex and GetSpecializationInfo(specIndex) or 0
        LifaMpLoop.currentClassID = classID
        LifaMpLoop.currentSpecID = specID

        LifaMpLoop:InitData(classID, specID)   -- Data.lua
        LifaMpLoop:InitUI()     -- UI.lua
        LifaMpLoop:InitLootMonitor() -- LootMonitor.lua
        --LifaMpLoop:HookCharacterFrame() -- Add tab to character frame
        LifaMpLoop.loaded = true

        -- Print welcome
        --print("|cff00ccff[Lifa M+ Loop]|r " .. LPL["SLASH_HELP"])

        self:UnregisterEvent("PLAYER_LOGIN")
    end
end)

--------------------------------------------------------------
-- Slash commands
--------------------------------------------------------------
SLASH_LIFAMPLOOP1 = "/mpl"
SLASH_LIFAMPLOOP2 = "/mploop"
SlashCmdList["LIFAMPLOOP"] = function(msg)
    msg = strtrim(msg):lower()
    if msg == "watch" or msg == "watchlist" then
        LifaMpLoop:ToggleMainFrame(4) -- tab 4 = watchlist
    elseif msg == "test" then
        LifaMpLoop.testMode = not LifaMpLoop.testMode
        if LifaMpLoop.testMode then
            print("|cffFFFF00[Lifa M+ Loop]|r 测试模式已开启 - 队友拾取任何物品都会触发密语，并打印详细信息")
        else
            print("|cff00ccff[Lifa M+ Loop]|r 测试模式已关闭")
        end
    else
        LifaMpLoop:ToggleMainFrame()
    end
end

--------------------------------------------------------------
-- Utility helpers
--------------------------------------------------------------
function LifaMpLoop:IsItemWatched(itemID)
    return self.db and self.db.watchlist and self.db.watchlist[itemID] ~= nil
end

function LifaMpLoop:AddToWatchlist(itemID, dungeonName, bossName)
    if not itemID or not self.db then return end
    self.db.watchlist[itemID] = {
        itemID = itemID,
        dungeonName = dungeonName or "",
        bossName = bossName or "",
        addedTime = time(),
    }
    if self.RefreshWatchlistTab then
        self:RefreshWatchlistTab()
    end
end

function LifaMpLoop:RemoveFromWatchlist(itemID)
    if not itemID or not self.db then return end
    self.db.watchlist[itemID] = nil
    if self.RefreshWatchlistTab then
        self:RefreshWatchlistTab()
    end
end

--------------------------------------------------------------
-- Hook into Character Frame: add a tab button
--------------------------------------------------------------
function LifaMpLoop:HookCharacterFrame()
    local function TryHook()
        if not CharacterFrame then return false end

        local numTabs = CharacterFrame.numTabs or 3
        local newTabIndex = numTabs + 1
        local lastTab = _G["CharacterFrameTab" .. numTabs]
        if not lastTab then return false end

        -- Try PanelTabButtonTemplate first, fall back to manual creation
        local ok, tab = pcall(CreateFrame, "Button", "CharacterFrameTab" .. newTabIndex, CharacterFrame, "PanelTabButtonTemplate")
        if not ok or not tab then return false end

        tab:SetID(newTabIndex)
        tab:SetText(LPL["CHAR_FRAME_TAB"])
        PanelTemplates_TabResize(tab, 0)

        tab:SetScript("OnClick", function()
            LifaMpLoop:ToggleMainFrame()
        end)

        CharacterFrame.numTabs = newTabIndex
        PanelTemplates_DeselectTab(tab)

        -- Apply ElvUI skin if available (so our tab matches the reskinned native tabs)
        if ElvUI then
            local E = unpack(ElvUI)
            if E and E.Skins and E.Skins.HandleTab then
                pcall(E.Skins.HandleTab, E.Skins, tab)
            end
        end

        -- Position after skinning (ElvUI changes tab size/padding)
        tab:ClearAllPoints()
        local spacing = ElvUI and -6 or 4
        tab:SetPoint("LEFT", lastTab, "RIGHT", spacing, 0)

        -- Ensure text is visible after skinning (delay to next frame so ElvUI hooks finish)
        C_Timer.After(0, function()
            tab:SetText(LPL["CHAR_FRAME_TAB"])
            if tab.Text then
                tab.Text:SetText(LPL["CHAR_FRAME_TAB"])
            end
            local fs = tab:GetFontString()
            if fs then
                fs:SetText(LPL["CHAR_FRAME_TAB"])
                fs:Show()
            end
            PanelTemplates_TabResize(tab, 0)
        end)

        return true
    end

    if not TryHook() then
        -- CharacterFrame not loaded yet; wait for it
        local waiter = CreateFrame("Frame")
        waiter:RegisterEvent("ADDON_LOADED")
        waiter:SetScript("OnEvent", function(self, event, name)
            if name == "Blizzard_CharacterFrame" or name == "Blizzard_PlayerSpells" then
                C_Timer.After(0, function()
                    TryHook()
                end)
                self:UnregisterEvent("ADDON_LOADED")
            end
        end)
    end
end


--------------------------------------------------------------
-- LifaMpLoop  Data.lua  –  Load dungeon & loot data from EJ
--------------------------------------------------------------

-- SLOT mapping: invType string → category key
local INVTYPE_TO_SLOT = {
    INVTYPE_HEAD         = "HEAD",
    INVTYPE_NECK         = "NECK",
    INVTYPE_SHOULDER     = "SHOULDER",
    INVTYPE_CLOAK        = "BACK",
    INVTYPE_CHEST        = "CHEST",
    INVTYPE_ROBE         = "CHEST",
    INVTYPE_WRIST        = "WRIST",
    INVTYPE_HAND         = "HANDS",
    INVTYPE_WAIST        = "WAIST",
    INVTYPE_LEGS         = "LEGS",
    INVTYPE_FEET         = "FEET",
    INVTYPE_FINGER       = "FINGER",
    INVTYPE_TRINKET      = "TRINKET",
    INVTYPE_WEAPON       = "WEAPON",
    INVTYPE_2HWEAPON     = "WEAPON",
    INVTYPE_WEAPONMAINHAND = "WEAPON",
    INVTYPE_WEAPONOFFHAND  = "OFFHAND",
    INVTYPE_SHIELD       = "OFFHAND",
    INVTYPE_HOLDABLE     = "OFFHAND",
    INVTYPE_RANGED       = "WEAPON",
    INVTYPE_RANGEDRIGHT  = "WEAPON",
}

LifaMpLoop.INVTYPE_TO_SLOT = INVTYPE_TO_SLOT

-- Stat key → friendly key for our filters
local STAT_KEYS = {
    ITEM_MOD_CRIT_RATING_SHORT    = "CRIT",
    ITEM_MOD_HASTE_RATING_SHORT   = "HASTE",
    ITEM_MOD_MASTERY_RATING_SHORT = "MASTERY",
    ITEM_MOD_VERSATILITY          = "VERS",
}
LifaMpLoop.STAT_KEYS = STAT_KEYS

-- Reverse map
LifaMpLoop.STAT_INTERNAL = {
    CRIT    = "ITEM_MOD_CRIT_RATING_SHORT",
    HASTE   = "ITEM_MOD_HASTE_RATING_SHORT",
    MASTERY = "ITEM_MOD_MASTERY_RATING_SHORT",
    VERS    = "ITEM_MOD_VERSATILITY",
}

-- All slot categories in display order
LifaMpLoop.SLOT_ORDER = {
    "HEAD", "NECK", "SHOULDER", "BACK", "CHEST", "WRIST",
    "HANDS", "WAIST", "LEGS", "FEET", "FINGER", "TRINKET",
    "WEAPON", "OFFHAND",
}

-- Map slot key → localized name
LifaMpLoop.SLOT_LOCALE_KEY = {
    HEAD = "SLOT_HEAD", NECK = "SLOT_NECK", SHOULDER = "SLOT_SHOULDER",
    BACK = "SLOT_BACK", CHEST = "SLOT_CHEST", WRIST = "SLOT_WRIST",
    HANDS = "SLOT_HANDS", WAIST = "SLOT_WAIST", LEGS = "SLOT_LEGS",
    FEET = "SLOT_FEET", FINGER = "SLOT_FINGER", TRINKET = "SLOT_TRINKET",
    WEAPON = "SLOT_WEAPON", OFFHAND = "SLOT_OFFHAND",
}

--------------------------------------------------------------
-- Data store
--------------------------------------------------------------
LifaMpLoop.allLoot = {}
LifaMpLoop.dungeons = {}
LifaMpLoop.bySlot = {}
LifaMpLoop.byDungeon = {}
LifaMpLoop.mapIDToInstance = {}

local pendingItems = 0
local dataReady = false

-- Queue-driven encounter loader
local encounterQueue = {}     -- { {instanceID, encounterID, encounterName, dungeonName}, ... }
local queueIndex = 0
local eventFrame = CreateFrame("Frame")
local knownItemIDs = {}

-- Fresh EJ scan accumulation tables (separate from display data)
local freshLoot = {}
local freshBySlot = {}
local freshByDungeon = {}
local freshDungeons = {}

--------------------------------------------------------------
-- InitData: collect dungeon/encounter list, then start queue
--------------------------------------------------------------
function LifaMpLoop:InitData(classID, specID)
    self.allLoot = {}
    self.dungeons = {}
    self.bySlot = {}
    self.byDungeon = {}
    self.mapIDToInstance = {}
    knownItemIDs = {}
    encounterQueue = {}
    queueIndex = 0
    dataReady = false
    pendingItems = 0

    -- Fresh EJ scan tables (kept separate so cache data can display until scan completes)
    freshLoot = {}
    freshBySlot = {}
    freshByDungeon = {}
    freshDungeons = {}

    self.currentClassID = classID or self.currentClassID
    self.currentSpecID = specID or self.currentSpecID

    -- Try loading from cache first for instant display
    local cacheKey = (self.currentClassID or 0) .. "-" .. (self.currentSpecID or 0)
    if self.db and self.db.lootCache and self.db.lootCache[cacheKey] then
        local cache = self.db.lootCache[cacheKey]
        if cache.items and #cache.items > 0 then
            self:LoadFromCache(cache.items)
        end
    end

    if classID and specID then
        EJ_SetLootFilter(classID, specID)
    elseif classID then
        EJ_SetLootFilter(classID, 0)
    end

    local challengeMapIDs = C_ChallengeMode.GetMapTable() or {}
    local cmMaps = {}
    for _, cmID in ipairs(challengeMapIDs) do
        local name = C_ChallengeMode.GetMapUIInfo(cmID)
        if name then
            cmMaps[cmID] = name
        end
    end

    local numTiers = EJ_GetNumTiers()
    if numTiers and numTiers > 0 then
        EJ_SelectTier(numTiers)
    end
    EJ_SetDifficulty(23)

    -- Gather EJ dungeon instances
    local ejInstances = {}
    local idx = 1
    while true do
        local instanceID, instName, _, _, _, _, _, _, _, _, mapID =
            EJ_GetInstanceByIndex(idx, false)
        if not instanceID then break end
        ejInstances[#ejInstances + 1] = {
            instanceID = instanceID,
            name = instName,
            mapID = mapID,
        }
        idx = idx + 1
    end

    -- Match to current M+ season
    local matchedInstances = {}
    for _, ejInst in ipairs(ejInstances) do
        local matched = false
        for cmID, cmName in pairs(cmMaps) do
            if cmName == ejInst.name then
                matchedInstances[#matchedInstances + 1] = ejInst
                self.mapIDToInstance[cmID] = ejInst.instanceID
                matched = true
                break
            end
        end
        if not matched and ejInst.mapID then
            for cmID, cmName in pairs(cmMaps) do
                if cmID == ejInst.mapID then
                    matchedInstances[#matchedInstances + 1] = ejInst
                    self.mapIDToInstance[cmID] = ejInst.instanceID
                    break
                end
            end
        end
    end
    if #matchedInstances == 0 then
        matchedInstances = ejInstances
    end

    -- Build encounter queue: collect all instance/boss pairs
    for _, inst in ipairs(matchedInstances) do
        freshByDungeon[inst.instanceID] = {}

        local dungeonEntry = {
            instanceID = inst.instanceID,
            name = inst.name,
            bosses = {},
        }

        EJ_SelectInstance(inst.instanceID)
        local bossIdx = 1
        while true do
            local encounterName, _, journalEncounterID = EJ_GetEncounterInfoByIndex(bossIdx)
            if not encounterName then break end

            dungeonEntry.bosses[#dungeonEntry.bosses + 1] = {
                name = encounterName,
                journalEncounterID = journalEncounterID,
            }

            encounterQueue[#encounterQueue + 1] = {
                instanceID = inst.instanceID,
                encounterID = journalEncounterID,
                encounterName = encounterName,
                dungeonName = inst.name,
            }

            bossIdx = bossIdx + 1
        end

        freshDungeons[#freshDungeons + 1] = dungeonEntry
    end

    -- If no cache was loaded, use the fresh dungeon list for immediate sidebar display
    if not dataReady then
        self.dungeons = freshDungeons
    end

    -- Start processing the queue
    self:ProcessNextEncounter()
end

--------------------------------------------------------------
-- Queue processor: select encounter → wait for data → collect loot → next
--------------------------------------------------------------
function LifaMpLoop:ProcessNextEncounter()
    queueIndex = queueIndex + 1

    if queueIndex > #encounterQueue then
        -- All encounters processed, wait for pending item loads
        eventFrame:UnregisterEvent("EJ_LOOT_DATA_RECIEVED")
        eventFrame:SetScript("OnEvent", nil)
        if pendingItems <= 0 then
            self:FinalizeData()
        end
        -- else: FinalizeData will be called when last item finishes loading
        return
    end

    local enc = encounterQueue[queueIndex]

    -- Set EJ state for this encounter
    EJ_SelectInstance(enc.instanceID)
    EJ_SetDifficulty(23)
    if self.currentClassID then
        EJ_SetLootFilter(self.currentClassID, self.currentSpecID or 0)
    end
    EJ_SelectEncounter(enc.encounterID)

    -- Try to collect loot immediately (may already be cached)
    local collected = self:CollectEncounterLoot(enc)

    if collected then
        -- Data was available, move to next encounter immediately
        self:ProcessNextEncounter()
    else
        -- Data not ready, wait for EJ_LOOT_DATA_RECIEVED
        eventFrame:RegisterEvent("EJ_LOOT_DATA_RECIEVED")
        eventFrame:SetScript("OnEvent", function(frame, event)
            if event == "EJ_LOOT_DATA_RECIEVED" then
                -- Re-select this encounter (event may have loaded our data)
                EJ_SelectInstance(enc.instanceID)
                EJ_SetDifficulty(23)
                if LifaMpLoop.currentClassID then
                    EJ_SetLootFilter(LifaMpLoop.currentClassID, LifaMpLoop.currentSpecID or 0)
                end
                EJ_SelectEncounter(enc.encounterID)

                local success = LifaMpLoop:CollectEncounterLoot(enc)
                if success then
                    eventFrame:UnregisterEvent("EJ_LOOT_DATA_RECIEVED")
                    eventFrame:SetScript("OnEvent", nil)
                    LifaMpLoop:ProcessNextEncounter()
                end
                -- else: keep waiting for more data events
            end
        end)
    end
end

--------------------------------------------------------------
-- Collect loot for one encounter. Returns true if data is available.
--------------------------------------------------------------
function LifaMpLoop:CollectEncounterLoot(enc)
    local numLoot = EJ_GetNumLoot() or 0
    if numLoot == 0 then
        return false
    end

    -- Check if all items have valid itemIDs and links
    local allValid = true
    local newEntries = {}

    for lootIdx = 1, numLoot do
        local info = C_EncounterJournal.GetLootInfoByIndex(lootIdx)
        if not info or not info.itemID or not info.link then
            allValid = false
        elseif not knownItemIDs[info.itemID] then
            newEntries[#newEntries + 1] = {
                itemID = info.itemID,
                name = info.name or "",
                icon = info.icon,
                link = info.link,
                slot = "",
                slotKey = "",
                stats = {},
                rawStats = {},
                dungeonName = enc.dungeonName,
                bossName = enc.encounterName,
                instanceID = enc.instanceID,
                encounterID = enc.encounterID,
                ilvl = 0,
            }
        end
    end

    if not allValid then
        return false
    end

    -- All items valid, mark as known and add to fresh accumulation tables
    for _, entry in ipairs(newEntries) do
        knownItemIDs[entry.itemID] = true
        freshLoot[#freshLoot + 1] = entry
        self:DeferItemLoad(entry)
    end

    return true
end

--------------------------------------------------------------
-- Called when all encounters scanned + all items loaded
--------------------------------------------------------------
function LifaMpLoop:FinalizeData()
    -- Swap fresh data into main tables
    self.allLoot = freshLoot
    self.bySlot = freshBySlot
    self.byDungeon = freshByDungeon
    self.dungeons = freshDungeons

    -- Fix any items that got generic links (missing difficulty bonuses)
    self:FixMissingEJLinks()
    dataReady = true

    -- Save to cache
    self:SaveToCache()

    if self.OnDataReady then
        self:OnDataReady()
    end
end

--------------------------------------------------------------
-- Deferred item loading (items may not be cached yet)
--------------------------------------------------------------
function LifaMpLoop:DeferItemLoad(entry)
    pendingItems = pendingItems + 1
    -- Preserve the EJ link (carries difficulty context → correct ilvl/stats)
    local ejLink = entry.link
    local item = Item:CreateFromItemID(entry.itemID)
    item:ContinueOnItemLoad(function()
        -- Use EJ link for stats/ilvl; fall back to generic link for name/icon only
        local genericLink = item:GetItemLink()
        entry.name = item:GetItemName() or entry.name
        entry.icon = item:GetItemIcon() or entry.icon
        -- Prefer EJ link (has difficulty bonus IDs) over generic link
        entry.link = ejLink or genericLink or entry.link

        -- Get item equip slot
        local _, _, _, itemEquipLoc, _, _, _ = C_Item.GetItemInfoInstant(entry.itemID)
        if itemEquipLoc and INVTYPE_TO_SLOT[itemEquipLoc] then
            entry.slotKey = INVTYPE_TO_SLOT[itemEquipLoc]
        end

        -- Item level – use EJ link for correct M+ ilvl
        local effectiveILvl = GetDetailedItemLevelInfo(entry.link or entry.itemID)
        entry.ilvl = effectiveILvl or 0

        -- Stats – use EJ link for correct M+ stat values
        if entry.link then
            local statsTable = C_Item.GetItemStats(entry.link)
            if statsTable then
                entry.rawStats = statsTable
                for statKey, friendlyKey in pairs(STAT_KEYS) do
                    if statsTable[statKey] then
                        entry.stats[friendlyKey] = statsTable[statKey]
                    end
                end
            end
        end

        -- Index by slot (into fresh tables)
        if entry.slotKey ~= "" then
            if not freshBySlot[entry.slotKey] then
                freshBySlot[entry.slotKey] = {}
            end
            freshBySlot[entry.slotKey][#freshBySlot[entry.slotKey] + 1] = entry
        end

        -- Index by dungeon (into fresh tables)
        if freshByDungeon[entry.instanceID] then
            freshByDungeon[entry.instanceID][#freshByDungeon[entry.instanceID] + 1] = entry
        end

        pendingItems = pendingItems - 1
        if pendingItems <= 0 then
            pendingItems = 0
            -- If queue is done, finalize; otherwise queue processor handles it
            if queueIndex > #encounterQueue then
                LifaMpLoop:FinalizeData()
            end
        end
    end)
end

--------------------------------------------------------------
-- Second pass: re-query EJ for items that had nil links
-- (items are now in client cache, so EJ should return proper links)
--------------------------------------------------------------
function LifaMpLoop:FixMissingEJLinks()
    -- Restore EJ filter state
    local numTiers = EJ_GetNumTiers()
    if numTiers and numTiers > 0 then
        EJ_SelectTier(numTiers)
    end
    EJ_SetDifficulty(23)
    if self.currentClassID then
        EJ_SetLootFilter(self.currentClassID, self.currentSpecID or 0)
    end

    -- Group entries by instanceID + encounterID to minimize EJ state switches
    local needsFix = {}
    for _, entry in ipairs(self.allLoot) do
        -- Detect items with incorrect/missing links (low ilvl = no difficulty bonus)
        if entry.ilvl < 200 or not entry.link then
            local key = entry.instanceID .. "-" .. entry.encounterID
            if not needsFix[key] then
                needsFix[key] = { instanceID = entry.instanceID, encounterID = entry.encounterID, entries = {} }
            end
            needsFix[key].entries[#needsFix[key].entries + 1] = entry
        end
    end

    for _, group in pairs(needsFix) do
        EJ_SelectInstance(group.instanceID)
        EJ_SetDifficulty(23)
        EJ_SelectEncounter(group.encounterID)

        -- Build a lookup of items from EJ by itemID
        local numLoot = EJ_GetNumLoot() or 0
        local ejLookup = {}
        for idx = 1, numLoot do
            local info = C_EncounterJournal.GetLootInfoByIndex(idx)
            if info and info.itemID and info.link then
                ejLookup[info.itemID] = info.link
            end
        end

        -- Patch each entry
        for _, entry in ipairs(group.entries) do
            local newLink = ejLookup[entry.itemID]
            if newLink then
                entry.link = newLink
                local effectiveILvl = GetDetailedItemLevelInfo(newLink)
                entry.ilvl = effectiveILvl or entry.ilvl
                -- Re-compute stats
                local statsTable = C_Item.GetItemStats(newLink)
                if statsTable then
                    entry.rawStats = statsTable
                    entry.stats = {}
                    for statKey, friendlyKey in pairs(STAT_KEYS) do
                        if statsTable[statKey] then
                            entry.stats[friendlyKey] = statsTable[statKey]
                        end
                    end
                end
            end
        end
    end
end

function LifaMpLoop:IsDataReady()
    return dataReady
end

--------------------------------------------------------------
-- Sorting by stat priority (bitwise approach)
--------------------------------------------------------------
-- statPriority: ordered list of internal stat keys, index 1 = highest bit
-- Example: {CRIT, MASTERY, VERS, HASTE}, item with HASTE+MASTERY → bits = 0101
-- 1) Higher bit pattern ranks first
-- 2) Same bit pattern → compare stat values in priority order, higher first
function LifaMpLoop:SortByStatPriority(items, statPriority)
    if not statPriority or #statPriority == 0 then return items end

    -- Map each priority position to friendly key and bit value
    local n = #statPriority
    local bitOf = {}     -- friendlyKey → bit value (highest priority = highest bit)
    local prioKeys = {}  -- ordered friendly keys
    for i, internalKey in ipairs(statPriority) do
        local fk = STAT_KEYS[internalKey]
        if fk then
            bitOf[fk] = bit.lshift(1, n - i)  -- pos 1 → highest bit
            prioKeys[#prioKeys + 1] = fk
        end
    end

    -- Compute bit pattern for an item
    local function bitPattern(entry)
        local bits = 0
        for fk, bv in pairs(bitOf) do
            if entry.stats[fk] then
                bits = bits + bv
            end
        end
        return bits
    end

    table.sort(items, function(a, b)
        local ba, bb = bitPattern(a), bitPattern(b)
        if ba ~= bb then
            return ba > bb
        end
        -- Same bit pattern → compare stat values in priority order
        for _, fk in ipairs(prioKeys) do
            local va = a.stats[fk] or 0
            local vb = b.stats[fk] or 0
            if va ~= vb then
                return va > vb
            end
        end
        -- Stable tiebreaker
        return (a.itemID or 0) < (b.itemID or 0)
    end)

    return items
end

--------------------------------------------------------------
-- Filter helpers
--------------------------------------------------------------
function LifaMpLoop:FilterBySlot(slotKey)
    return self.bySlot[slotKey] or {}
end

function LifaMpLoop:FilterByDungeon(instanceID)
    return self.byDungeon[instanceID] or {}
end

-- Filter items that have at least one of the selected stats
-- Filter items: all secondary stats on the item must be in the enabled list
function LifaMpLoop:FilterByStats(enabledStats)
    local enabled = {}
    for _, statFriendly in ipairs(enabledStats) do
        enabled[statFriendly] = true
    end
    local result = {}
    for _, entry in ipairs(self.allLoot) do
        local dominated = false
        local hasAny = false
        for fk, _ in pairs(entry.stats) do
            hasAny = true
            if not enabled[fk] then
                dominated = true
                break
            end
        end
        if hasAny and not dominated then
            result[#result + 1] = entry
        end
    end
    return result
end

--------------------------------------------------------------
-- Cache: save current loot data to SavedVariables
--------------------------------------------------------------
function LifaMpLoop:SaveToCache()
    if not self.db then return end
    if not self.db.lootCache then self.db.lootCache = {} end

    local cacheKey = (self.currentClassID or 0) .. "-" .. (self.currentSpecID or 0)
    local items = {}
    for _, entry in ipairs(self.allLoot) do
        items[#items + 1] = {
            itemID = entry.itemID,
            name = entry.name,
            icon = entry.icon,
            link = entry.link,
            slotKey = entry.slotKey,
            stats = entry.stats,
            rawStats = entry.rawStats,
            dungeonName = entry.dungeonName,
            bossName = entry.bossName,
            instanceID = entry.instanceID,
            encounterID = entry.encounterID,
            ilvl = entry.ilvl,
        }
    end
    self.db.lootCache[cacheKey] = {
        items = items,
        time = time(),
    }
end

--------------------------------------------------------------
-- Cache: load loot data from SavedVariables (instant display)
--------------------------------------------------------------
function LifaMpLoop:LoadFromCache(cachedItems)
    self.allLoot = {}
    self.bySlot = {}
    self.byDungeon = {}
    local dungeonSet = {}

    for _, ci in ipairs(cachedItems) do
        local entry = {
            itemID = ci.itemID,
            name = ci.name or "",
            icon = ci.icon,
            link = ci.link,
            slot = "",
            slotKey = ci.slotKey or "",
            stats = ci.stats or {},
            rawStats = ci.rawStats or {},
            dungeonName = ci.dungeonName or "",
            bossName = ci.bossName or "",
            instanceID = ci.instanceID or 0,
            encounterID = ci.encounterID or 0,
            ilvl = ci.ilvl or 0,
        }
        self.allLoot[#self.allLoot + 1] = entry

        -- Index by slot
        if entry.slotKey ~= "" then
            if not self.bySlot[entry.slotKey] then
                self.bySlot[entry.slotKey] = {}
            end
            self.bySlot[entry.slotKey][#self.bySlot[entry.slotKey] + 1] = entry
        end

        -- Index by dungeon
        if entry.instanceID and entry.instanceID ~= 0 then
            if not self.byDungeon[entry.instanceID] then
                self.byDungeon[entry.instanceID] = {}
            end
            self.byDungeon[entry.instanceID][#self.byDungeon[entry.instanceID] + 1] = entry
        end

        -- Collect dungeon info
        if entry.instanceID and entry.instanceID ~= 0 and not dungeonSet[entry.instanceID] then
            dungeonSet[entry.instanceID] = entry.dungeonName
        end
    end

    -- Rebuild dungeons list from cached data
    self.dungeons = {}
    for instID, instName in pairs(dungeonSet) do
        self.dungeons[#self.dungeons + 1] = {
            instanceID = instID,
            name = instName or "",
            bosses = {},
        }
    end
    table.sort(self.dungeons, function(a, b) return a.name < b.name end)

    dataReady = true
    if self.OnDataReady then
        self:OnDataReady()
    end
end

--------------------------------------------------------------
-- LifaMpLoop  UI.lua  –  Main frame, tabs, shared UI helpers
--------------------------------------------------------------

local FRAME_WIDTH = 750
local FRAME_HEIGHT = 520
local TAB_HEIGHT = 28
local ITEM_ROW_HEIGHT = 42
local CONTENT_INSET = 8

LifaMpLoop.UI = {}

--------------------------------------------------------------
-- Main frame
--------------------------------------------------------------
local mainFrame

local function CreateMainFrame()
    local f = CreateFrame("Frame", "LifaMpLoopMainFrame", UIParent, "PortraitFrameTemplate")
    f:SetSize(FRAME_WIDTH, FRAME_HEIGHT)
    f:SetPoint("CENTER")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetFrameStrata("HIGH")
    f:SetClampedToScreen(true)

    -- Title
    f:SetTitle(LPL["ADDON_NAME"])

    -- Portrait icon
    f.PortraitContainer.portrait:SetTexture("Interface\\Icons\\inv_misc_key_15")

    -- Close button is auto-created by PortraitFrameTemplate

    -- Tab container (below portrait area, left-aligned)
    f.tabContainer = CreateFrame("Frame", nil, f)
    f.tabContainer:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -58)
    f.tabContainer:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -58)
    f.tabContainer:SetHeight(TAB_HEIGHT)

    -- Content area (below tabs, full width)
    f.content = CreateFrame("Frame", nil, f)
    f.content:SetPoint("TOPLEFT", f.tabContainer, "BOTTOMLEFT", 0, -2)
    f.content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -CONTENT_INSET, CONTENT_INSET)

    -- Nav area (top-right, beside portrait)
    f.navArea = CreateFrame("Frame", nil, f)
    f.navArea:SetPoint("TOPLEFT", f, "TOPLEFT", 64, -24)
    f.navArea:SetPoint("TOPRIGHT", f, "TOPRIGHT", -28, -24)
    f.navArea:SetHeight(30)

    f.tabs = {}
    f.tabFrames = {}
    f.activeTab = nil

    f:Hide()
    return f
end

--------------------------------------------------------------
-- Class/Spec dropdown (top-right of main frame)
--------------------------------------------------------------
local function BuildClassSpecData()
    local data = {} -- { { classID, className, classColor, specs = { {specID, specName}, ... } }, ... }
    for classID = 1, GetNumClasses() do
        local classInfo = C_CreatureInfo.GetClassInfo(classID)
        if classInfo then
            local classColor = C_ClassColor.GetClassColor(classInfo.classFile)
            local entry = {
                classID = classID,
                className = classInfo.className,
                classFile = classInfo.classFile,
                color = classColor,
                specs = {},
            }
            for specIdx = 1, GetNumSpecializationsForClassID(classID) do
                local specID, specName = GetSpecializationInfoForClassID(classID, specIdx)
                if specID then
                    entry.specs[#entry.specs + 1] = { specID = specID, specName = specName }
                end
            end
            data[#data + 1] = entry
        end
    end
    return data
end

local function CreateClassSpecDropdown(parent)
    local dropdown = CreateFrame("Button", nil, parent)
    dropdown:SetSize(180, 22)
    dropdown:SetPoint("RIGHT", parent, "RIGHT", 0, 0)

    dropdown.bg = dropdown:CreateTexture(nil, "BACKGROUND")
    dropdown.bg:SetAllPoints()
    dropdown.bg:SetColorTexture(0.1, 0.1, 0.1, 0.85)

    dropdown.border = dropdown:CreateTexture(nil, "BORDER")
    dropdown.border:SetPoint("TOPLEFT", -1, 1)
    dropdown.border:SetPoint("BOTTOMRIGHT", 1, -1)
    dropdown.border:SetColorTexture(0.35, 0.35, 0.35, 1)

    dropdown.text = dropdown:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    dropdown.text:SetPoint("LEFT", 6, 0)
    dropdown.text:SetPoint("RIGHT", -18, 0)
    dropdown.text:SetJustifyH("LEFT")
    dropdown.text:SetWordWrap(false)

    dropdown.arrow = dropdown:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    dropdown.arrow:SetPoint("RIGHT", -4, 0)
    dropdown.arrow:SetText("\226\150\188")

    -- Menu frame (two-level: class list → spec sub-list)
    local menu = CreateFrame("Frame", "LifaMpLoopClassSpecMenu", dropdown)
    menu:SetFrameStrata("FULLSCREEN_DIALOG")
    menu:SetClampedToScreen(true)
    menu.bg = menu:CreateTexture(nil, "BACKGROUND")
    menu.bg:SetAllPoints()
    menu.bg:SetColorTexture(0.08, 0.08, 0.08, 0.95)
    menu:EnableMouse(true)
    menu:Hide()

    local subMenu = CreateFrame("Frame", "LifaMpLoopSpecSubMenu", menu)
    subMenu:SetFrameStrata("FULLSCREEN_DIALOG")
    subMenu:SetClampedToScreen(true)
    subMenu.bg = subMenu:CreateTexture(nil, "BACKGROUND")
    subMenu.bg:SetAllPoints()
    subMenu.bg:SetColorTexture(0.08, 0.08, 0.08, 0.95)
    subMenu:EnableMouse(true)
    subMenu:Hide()

    local classSpecData = BuildClassSpecData()
    local classButtons = {}
    local specButtons = {}

    local BTN_W, BTN_H = 160, 18

    -- Build class buttons
    for i, cls in ipairs(classSpecData) do
        local btn = CreateFrame("Button", nil, menu)
        btn:SetSize(BTN_W, BTN_H)
        btn:SetPoint("TOPLEFT", menu, "TOPLEFT", 2, -(i - 1) * BTN_H - 2)

        btn.highlight = btn:CreateTexture(nil, "HIGHLIGHT")
        btn.highlight:SetAllPoints()
        btn.highlight:SetColorTexture(1, 1, 1, 0.1)

        btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        btn.text:SetPoint("LEFT", 4, 0)
        btn.text:SetText(cls.className)
        if cls.color then
            btn.text:SetTextColor(cls.color.r, cls.color.g, cls.color.b)
        end

        btn.arrowText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        btn.arrowText:SetPoint("RIGHT", -2, 0)
        btn.arrowText:SetText("|TInterface\\ChatFrame\\ChatFrameExpandArrow:12:12|t")
        btn.arrowText:SetTextColor(0.5, 0.5, 0.5)

        btn:SetScript("OnEnter", function(self)
            -- Show spec sub-menu
            for _, sb in ipairs(specButtons) do sb:Hide() end
            subMenu:SetPoint("TOPLEFT", self, "TOPRIGHT", 1, 0)

            -- "All specs" option
            if not specButtons[0] then
                local allBtn = CreateFrame("Button", nil, subMenu)
                allBtn:SetSize(BTN_W, BTN_H)
                allBtn.highlight = allBtn:CreateTexture(nil, "HIGHLIGHT")
                allBtn.highlight:SetAllPoints()
                allBtn.highlight:SetColorTexture(1, 1, 1, 0.1)
                allBtn.text = allBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                allBtn.text:SetPoint("LEFT", 4, 0)
                specButtons[0] = allBtn
            end
            local allBtn = specButtons[0]
            allBtn:SetPoint("TOPLEFT", subMenu, "TOPLEFT", 2, -2)
            allBtn.text:SetText(LifaMpLoop.LPL["ALL_SPECS"])
            if cls.color then
                allBtn.text:SetTextColor(cls.color.r, cls.color.g, cls.color.b)
            end
            allBtn:SetScript("OnClick", function()
                LifaMpLoop.currentClassID = cls.classID
                LifaMpLoop.currentSpecID = 0
                local label = cls.className
                if cls.color then
                    dropdown.text:SetText(cls.color:WrapTextInColorCode(label))
                else
                    dropdown.text:SetText(label)
                end
                menu:Hide(); subMenu:Hide()
                LifaMpLoop:ReloadDataForFilter(cls.classID, 0)
            end)
            allBtn:Show()

            local specCount = #cls.specs
            for si, spec in ipairs(cls.specs) do
                local sbtn = specButtons[si]
                if not sbtn then
                    sbtn = CreateFrame("Button", nil, subMenu)
                    sbtn:SetSize(BTN_W, BTN_H)
                    sbtn.highlight = sbtn:CreateTexture(nil, "HIGHLIGHT")
                    sbtn.highlight:SetAllPoints()
                    sbtn.highlight:SetColorTexture(1, 1, 1, 0.1)
                    sbtn.text = sbtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                    sbtn.text:SetPoint("LEFT", 4, 0)
                    specButtons[si] = sbtn
                end
                sbtn:SetPoint("TOPLEFT", subMenu, "TOPLEFT", 2, -si * BTN_H - 2)
                sbtn.text:SetText(spec.specName)
                if cls.color then
                    sbtn.text:SetTextColor(cls.color.r, cls.color.g, cls.color.b)
                end
                sbtn:SetScript("OnClick", function()
                    LifaMpLoop.currentClassID = cls.classID
                    LifaMpLoop.currentSpecID = spec.specID
                    local label = cls.className .. " - " .. spec.specName
                    if cls.color then
                        dropdown.text:SetText(cls.color:WrapTextInColorCode(label))
                    else
                        dropdown.text:SetText(label)
                    end
                    menu:Hide(); subMenu:Hide()
                    LifaMpLoop:ReloadDataForFilter(cls.classID, spec.specID)
                end)
                sbtn:Show()
            end

            subMenu:SetSize(BTN_W + 4, (specCount + 1) * BTN_H + 4)
            subMenu:Show()
        end)

        classButtons[i] = btn
    end

    menu:SetSize(BTN_W + 4, #classSpecData * BTN_H + 4)
    menu:SetPoint("TOPRIGHT", dropdown, "BOTTOMRIGHT", 0, -1)

    dropdown:SetScript("OnClick", function()
        if menu:IsShown() then
            menu:Hide(); subMenu:Hide()
        else
            menu:Show()
        end
    end)

    -- Auto-close on outside click
    menu:SetScript("OnUpdate", function(self)
        if not self:IsMouseOver() and not subMenu:IsMouseOver() and not dropdown:IsMouseOver() then
            if IsMouseButtonDown("LeftButton") then
                self:Hide(); subMenu:Hide()
            end
        end
    end)

    -- Set initial text
    local function SetInitialLabel()
        local cID = LifaMpLoop.currentClassID
        local sID = LifaMpLoop.currentSpecID
        for _, cls in ipairs(classSpecData) do
            if cls.classID == cID then
                local label = cls.className
                for _, spec in ipairs(cls.specs) do
                    if spec.specID == sID then
                        label = cls.className .. " - " .. spec.specName
                        break
                    end
                end
                if cls.color then
                    dropdown.text:SetText(cls.color:WrapTextInColorCode(label))
                else
                    dropdown.text:SetText(label)
                end
                return
            end
        end
        dropdown.text:SetText("")
    end
    SetInitialLabel()

    return dropdown
end

--------------------------------------------------------------
-- Tab system (custom buttons, no legacy templates)
--------------------------------------------------------------
local TAB_WIDTH = 80
local TAB_SELECTED_COLOR = { 0.15, 0.15, 0.15, 1 }
local TAB_NORMAL_COLOR   = { 0.05, 0.05, 0.05, 0.8 }
local TAB_TEXT_SELECTED   = { 1, 0.82, 0 }
local TAB_TEXT_NORMAL     = { 0.7, 0.7, 0.7 }

local function SelectTabVisual(tab)
    tab.bg:SetColorTexture(unpack(TAB_SELECTED_COLOR))
    tab.text:SetTextColor(unpack(TAB_TEXT_SELECTED))
    tab.bottomLine:Hide()
end

local function DeselectTabVisual(tab)
    tab.bg:SetColorTexture(unpack(TAB_NORMAL_COLOR))
    tab.text:SetTextColor(unpack(TAB_TEXT_NORMAL))
    tab.bottomLine:Show()
end

local function CreateTab(parent, index, text)
    local tab = CreateFrame("Button", "LifaMpLoopTab" .. index, parent.tabContainer)
    tab:SetID(index)
    tab:SetSize(TAB_WIDTH, TAB_HEIGHT)

    tab.bg = tab:CreateTexture(nil, "BACKGROUND")
    tab.bg:SetAllPoints()
    tab.bg:SetColorTexture(unpack(TAB_NORMAL_COLOR))

    tab.highlight = tab:CreateTexture(nil, "HIGHLIGHT")
    tab.highlight:SetAllPoints()
    tab.highlight:SetColorTexture(1, 1, 1, 0.08)

    tab.bottomLine = tab:CreateTexture(nil, "BORDER")
    tab.bottomLine:SetHeight(1)
    tab.bottomLine:SetPoint("BOTTOMLEFT"); tab.bottomLine:SetPoint("BOTTOMRIGHT")
    tab.bottomLine:SetColorTexture(0.4, 0.4, 0.4, 1)

    tab.text = tab:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    tab.text:SetPoint("CENTER", 0, 1)
    tab.text:SetText(text)
    tab.text:SetTextColor(unpack(TAB_TEXT_NORMAL))

    tab:SetScript("OnClick", function(self)
        LifaMpLoop:SelectTab(self:GetID())
    end)

    if index == 1 then
        tab:SetPoint("BOTTOMLEFT", parent.tabContainer, "BOTTOMLEFT", 0, 0)
    else
        tab:SetPoint("LEFT", parent.tabs[index - 1], "RIGHT", 2, 0)
    end

    parent.tabs[index] = tab
    return tab
end

function LifaMpLoop:SelectTab(tabIndex)
    if not mainFrame then return end
    for i, frame in pairs(mainFrame.tabFrames) do
        if frame then frame:Hide() end
    end
    for i, tab in pairs(mainFrame.tabs) do
        DeselectTabVisual(tab)
    end

    if mainFrame.tabFrames[tabIndex] then
        mainFrame.tabFrames[tabIndex]:Show()
    end
    SelectTabVisual(mainFrame.tabs[tabIndex])
    mainFrame.activeTab = tabIndex

    -- Trigger refresh
    if tabIndex == 1 and self.RefreshSlotTab then
        self:RefreshSlotTab()
    elseif tabIndex == 2 and self.RefreshStatsTab then
        self:RefreshStatsTab()
    elseif tabIndex == 3 and self.RefreshDungeonTab then
        self:RefreshDungeonTab()
    elseif tabIndex == 4 and self.RefreshWatchlistTab then
        self:RefreshWatchlistTab()
    end
end

--------------------------------------------------------------
-- Create a tab content frame
--------------------------------------------------------------
local function CreateTabContentFrame(parent, index)
    local f = CreateFrame("Frame", nil, parent.content)
    f:SetAllPoints(parent.content)
    f:Hide()
    parent.tabFrames[index] = f
    return f
end

--------------------------------------------------------------
-- Shared: Scrollable item list
--------------------------------------------------------------
function LifaMpLoop:CreateScrollableList(parent)
    local scrollFrame = CreateFrame("ScrollFrame", nil, parent, "ScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, -4)
    scrollFrame:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -24, 4)

    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetWidth(scrollFrame:GetWidth() or (FRAME_WIDTH - 60))
    scrollFrame:SetScrollChild(scrollChild)

    scrollFrame.scrollChild = scrollChild
    scrollFrame.rows = {}

    return scrollFrame
end

--------------------------------------------------------------
-- Shared: Create an item row
--------------------------------------------------------------
function LifaMpLoop:CreateItemRow(parent, index)
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(ITEM_ROW_HEIGHT)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -(index - 1) * ITEM_ROW_HEIGHT)
    row:SetPoint("RIGHT", parent, "RIGHT", 0, 0)
    row:EnableMouse(true)

    -- Background highlight
    row.highlight = row:CreateTexture(nil, "HIGHLIGHT")
    row.highlight:SetAllPoints()
    row.highlight:SetColorTexture(1, 1, 1, 0.05)

    -- Watching indicator (left bar)
    row.watchBar = row:CreateTexture(nil, "OVERLAY")
    row.watchBar:SetSize(3, ITEM_ROW_HEIGHT - 4)
    row.watchBar:SetPoint("LEFT", row, "LEFT", 0, 0)
    row.watchBar:SetColorTexture(0, 0.8, 1, 1)
    row.watchBar:Hide()

    -- Icon
    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(ITEM_ROW_HEIGHT - 6, ITEM_ROW_HEIGHT - 6)
    row.icon:SetPoint("LEFT", row, "LEFT", 8, 0)
    row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    -- Item name
    row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.nameText:SetPoint("TOPLEFT", row.icon, "TOPRIGHT", 8, -2)
    row.nameText:SetPoint("RIGHT", row, "RIGHT", -180, 0)
    row.nameText:SetJustifyH("LEFT")
    row.nameText:SetWordWrap(false)

    -- Sub info (dungeon + boss)
    row.subText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.subText:SetPoint("TOPLEFT", row.nameText, "BOTTOMLEFT", 0, -2)
    row.subText:SetPoint("RIGHT", row, "RIGHT", -180, 0)
    row.subText:SetJustifyH("LEFT")
    row.subText:SetTextColor(0.6, 0.6, 0.6)
    row.subText:SetWordWrap(false)

    -- Stats text (right side)
    row.statsText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.statsText:SetPoint("RIGHT", row, "RIGHT", -8, 0)
    row.statsText:SetJustifyH("RIGHT")
    row.statsText:SetWidth(170)
    row.statsText:SetWordWrap(false)

    -- Tooltip on hover
    row:SetScript("OnEnter", function(self)
        if self.itemLink then
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetHyperlink(self.itemLink)
            if LifaMpLoop:IsItemWatched(self.itemID) then
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine(LPL["WATCHING"], 0, 0.8, 1)
            else
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine(LPL["CLICK_TO_WATCH"], 0.5, 0.5, 0.5)
            end
            GameTooltip:Show()
        end
    end)
    row:SetScript("OnLeave", function(self)
        GameTooltip:Hide()
    end)

    -- Right click: toggle watchlist
    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    row:SetScript("OnClick", function(self, button)
        if button == "RightButton" and self.itemID then
            if LifaMpLoop:IsItemWatched(self.itemID) then
                LifaMpLoop:RemoveFromWatchlist(self.itemID)
            else
                LifaMpLoop:AddToWatchlist(self.itemID, self.dungeonName, self.bossName)
            end
            -- Refresh current view
            if mainFrame and mainFrame.activeTab then
                LifaMpLoop:SelectTab(mainFrame.activeTab)
            end
        elseif button == "LeftButton" and self.itemLink then
            if IsModifiedClick("DRESSUP") then
                DressUpItemLink(self.itemLink)
            elseif IsModifiedClick("CHATLINK") then
                ChatEdit_InsertLink(self.itemLink)
            end
        end
    end)

    return row
end

--------------------------------------------------------------
-- Shared: Populate an item row with data
--------------------------------------------------------------
function LifaMpLoop:SetItemRowData(row, entry)
    row.itemID = entry.itemID
    row.itemLink = entry.link
    row.dungeonName = entry.dungeonName
    row.bossName = entry.bossName

    row.icon:SetTexture(entry.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
    row.nameText:SetText(entry.link or entry.name or ("item:" .. entry.itemID))

    local subParts = {}
    if entry.dungeonName and entry.dungeonName ~= "" then
        subParts[#subParts + 1] = entry.dungeonName
    end
    if entry.bossName and entry.bossName ~= "" then
        subParts[#subParts + 1] = entry.bossName
    end
    row.subText:SetText(table.concat(subParts, "  |  "))

    -- Build stats string: sorted by value descending, no numbers
    local statList = {}
    local statColors = {
        CRIT = "|cffFF4444",
        HASTE = "|cffFFFF00",
        MASTERY = "|cff44FF44",
        VERS = "|cff8888FF",
    }
    local statNames = {
        CRIT = LPL["STAT_CRIT"],
        HASTE = LPL["STAT_HASTE"],
        MASTERY = LPL["STAT_MASTERY"],
        VERS = LPL["STAT_VERSATILITY"],
    }
    for fk, val in pairs(entry.stats) do
        statList[#statList + 1] = { key = fk, val = val }
    end
    table.sort(statList, function(a, b) return a.val > b.val end)

    local statParts = {}
    for _, s in ipairs(statList) do
        local color = statColors[s.key] or "|cffFFFFFF"
        local name = statNames[s.key] or s.key
        statParts[#statParts + 1] = color .. name .. "|r"
    end
    row.statsText:SetText(table.concat(statParts, "  "))

    -- Watch indicator
    if LifaMpLoop:IsItemWatched(entry.itemID) then
        row.watchBar:Show()
    else
        row.watchBar:Hide()
    end

    row:Show()
end

--------------------------------------------------------------
-- Shared: Populate scroll list with entries
--------------------------------------------------------------
function LifaMpLoop:PopulateScrollList(scrollFrame, entries)
    local scrollChild = scrollFrame.scrollChild
    -- Hide all old rows
    for _, row in ipairs(scrollFrame.rows) do
        row:Hide()
    end

    if not entries or #entries == 0 then
        if not scrollFrame.emptyText then
            scrollFrame.emptyText = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontDisableLarge")
            scrollFrame.emptyText:SetPoint("CENTER", scrollChild, "CENTER", 0, 0)
        end
        scrollFrame.emptyText:SetText(LPL["NO_RESULTS"])
        scrollFrame.emptyText:Show()
        scrollChild:SetHeight(200)
        return
    end

    if scrollFrame.emptyText then
        scrollFrame.emptyText:Hide()
    end

    local width = scrollFrame:GetWidth() or (FRAME_WIDTH - 60)
    scrollChild:SetWidth(width)
    scrollChild:SetHeight(#entries * ITEM_ROW_HEIGHT + 10)

    for i, entry in ipairs(entries) do
        if not scrollFrame.rows[i] then
            scrollFrame.rows[i] = self:CreateItemRow(scrollChild, i)
        end
        local row = scrollFrame.rows[i]
        row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -(i - 1) * ITEM_ROW_HEIGHT)
        row:SetPoint("RIGHT", scrollChild, "RIGHT", 0, 0)
        self:SetItemRowData(row, entry)
    end
end

--------------------------------------------------------------
-- Main frame init and toggle
--------------------------------------------------------------
function LifaMpLoop:InitUI()
    mainFrame = CreateMainFrame()
    LifaMpLoop.mainFrame = mainFrame

    -- Create the four tabs
    CreateTab(mainFrame, 1, LPL["TAB_SLOT"])
    CreateTab(mainFrame, 2, LPL["TAB_STATS"])
    CreateTab(mainFrame, 3, LPL["TAB_DUNGEON"])
    CreateTab(mainFrame, 4, LPL["TAB_WATCHLIST"])

    -- Create tab content frames
    local slotFrame = CreateTabContentFrame(mainFrame, 1)
    local statsFrame = CreateTabContentFrame(mainFrame, 2)
    local dungeonFrame = CreateTabContentFrame(mainFrame, 3)
    local watchFrame = CreateTabContentFrame(mainFrame, 4)

    -- Let modules build their UI
    self:BuildSlotTab(slotFrame)
    self:BuildStatsTab(statsFrame)
    self:BuildDungeonTab(dungeonFrame)
    self:BuildWatchlistTab(watchFrame)

    -- Default tab
    self:SelectTab(1)

    -- ESC to close
    tinsert(UISpecialFrames, "LifaMpLoopMainFrame")

    -- Keystone level slider + reward display (left of dropdown)
    self:CreateKeystoneSlider(mainFrame.navArea)

    -- Class/Spec dropdown in the nav area (beside portrait)
    CreateClassSpecDropdown(mainFrame.navArea)
end

--------------------------------------------------------------
-- Keystone level slider in nav area
--------------------------------------------------------------
function LifaMpLoop:CreateKeystoneSlider(parent)
    LifaMpLoop.keystoneLevel = 10  -- default

    -- Container for slider + labels
    local container = CreateFrame("Frame", nil, parent)
    container:SetSize(280, 28)
    container:SetPoint("LEFT", parent, "LEFT", 0, 0)

    -- Keystone level label
    local keyLabel = container:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    keyLabel:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -1)
    LifaMpLoop.keyLabel = keyLabel

    -- Reward ilvl display (right side of container)
    local rewardText = container:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    rewardText:SetPoint("TOPRIGHT", container, "TOPRIGHT", 0, -1)
    LifaMpLoop.rewardText = rewardText

    -- Slider
    local slider = CreateFrame("Slider", nil, container, "MinimalSliderTemplate")
    slider:SetPoint("BOTTOMLEFT", container, "BOTTOMLEFT", 0, 0)
    slider:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 0)
    slider:SetHeight(14)
    slider:SetMinMaxValues(2, 15)
    slider:SetValueStep(1)
    slider:SetObeyStepOnDrag(true)
    slider:SetValue(LifaMpLoop.keystoneLevel)

    slider:SetScript("OnValueChanged", function(self, value)
        value = math.floor(value + 0.5)
        LifaMpLoop.keystoneLevel = value
        LifaMpLoop:UpdateKeystoneLevelDisplay()
    end)

    LifaMpLoop.keystoneSlider = slider
    self:UpdateKeystoneLevelDisplay()
end

function LifaMpLoop:UpdateKeystoneLevelDisplay()
    if not LifaMpLoop.keyLabel then return end
    LifaMpLoop.keyLabel:SetText(string.format(LPL["KEYSTONE_LEVEL"], LifaMpLoop.keystoneLevel))
    local dropLevel, vaultLevel = C_MythicPlus.GetRewardLevelFromKeystoneLevel(LifaMpLoop.keystoneLevel)
    local parts = {}
    if dropLevel then
        parts[#parts + 1] = string.format(LPL["DUNGEON_DROP_ILVL"], dropLevel)
    end
    if vaultLevel then
        parts[#parts + 1] = "|cff99ccff" .. string.format(LPL["VAULT_ILVL"], vaultLevel) .. "|r"
    end
    LifaMpLoop.rewardText:SetText(table.concat(parts, "  "))
end

function LifaMpLoop:ToggleMainFrame(tabIndex)
    if not mainFrame then return end
    if mainFrame:IsShown() then
        if tabIndex and mainFrame.activeTab ~= tabIndex then
            self:SelectTab(tabIndex)
        else
            mainFrame:Hide()
        end
    else
        mainFrame:Show()
        if tabIndex then
            self:SelectTab(tabIndex)
        end
    end
end

--------------------------------------------------------------
-- Data ready callback → refresh current tab
--------------------------------------------------------------
function LifaMpLoop:OnDataReady()
    -- Update keystone display in case API wasn't ready at init
    self:UpdateKeystoneLevelDisplay()
    if mainFrame and mainFrame:IsShown() and mainFrame.activeTab then
        self:SelectTab(mainFrame.activeTab)
    end
end
--------------------------------------------------------------
-- Reload data with new class/spec filter
--------------------------------------------------------------
function LifaMpLoop:ReloadDataForFilter(classID, specID)
    self:InitData(classID, specID)
    -- If data loaded synchronously (all cached), refresh now
    if self:IsDataReady() and mainFrame and mainFrame:IsShown() and mainFrame.activeTab then
        self:SelectTab(mainFrame.activeTab)
    end
end

--------------------------------------------------------------
-- LifaMpLoop  BrowseBySlot.lua  –  Browse loot by equipment slot
--------------------------------------------------------------

local slotFrame, scrollFrame
local selectedSlot = nil

--------------------------------------------------------------
-- Build the tab UI
--------------------------------------------------------------
function LifaMpLoop:BuildSlotTab(parent)
    slotFrame = parent

    -- Left: slot button list
    local sidebar = CreateFrame("Frame", nil, parent)
    sidebar:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
    sidebar:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 0, 0)
    sidebar:SetWidth(110)

    -- Background for sidebar
    local sidebarBg = sidebar:CreateTexture(nil, "BACKGROUND")
    sidebarBg:SetAllPoints()
    sidebarBg:SetColorTexture(0, 0, 0, 0.3)

    sidebar.buttons = {}
    for i, slotKey in ipairs(LifaMpLoop.SLOT_ORDER) do
        local btn = CreateFrame("Button", nil, sidebar)
        btn:SetSize(106, 24)
        btn:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 2, -(i - 1) * 26 - 4)

        btn.bg = btn:CreateTexture(nil, "BACKGROUND")
        btn.bg:SetAllPoints()
        btn.bg:SetColorTexture(0.2, 0.2, 0.2, 0.5)

        btn.highlight = btn:CreateTexture(nil, "HIGHLIGHT")
        btn.highlight:SetAllPoints()
        btn.highlight:SetColorTexture(1, 1, 1, 0.1)

        btn.selectedTex = btn:CreateTexture(nil, "BORDER")
        btn.selectedTex:SetAllPoints()
        btn.selectedTex:SetColorTexture(0, 0.5, 0.8, 0.3)
        btn.selectedTex:Hide()

        local localeKey = LifaMpLoop.SLOT_LOCALE_KEY[slotKey]
        btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        btn.text:SetPoint("CENTER")
        btn.text:SetText(LPL[localeKey] or slotKey)

        btn.slotKey = slotKey
        btn:SetScript("OnClick", function(self)
            selectedSlot = self.slotKey
            LifaMpLoop:RefreshSlotTab()
        end)

        sidebar.buttons[i] = btn
    end

    slotFrame.sidebar = sidebar

    -- Right: item scroll list
    local listArea = CreateFrame("Frame", nil, parent)
    listArea:SetPoint("TOPLEFT", sidebar, "TOPRIGHT", 4, 0)
    listArea:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)

    scrollFrame = LifaMpLoop:CreateScrollableList(listArea)
    slotFrame.scrollFrame = scrollFrame

    -- Default selection
    selectedSlot = LifaMpLoop.SLOT_ORDER[1]
end

--------------------------------------------------------------
-- Refresh
--------------------------------------------------------------
function LifaMpLoop:RefreshSlotTab()
    if not slotFrame or not slotFrame:IsShown() then return end

    -- Update sidebar highlights
    if slotFrame.sidebar then
        for _, btn in ipairs(slotFrame.sidebar.buttons) do
            if btn.slotKey == selectedSlot then
                btn.selectedTex:Show()
            else
                btn.selectedTex:Hide()
            end
        end
    end

    if not self:IsDataReady() then
        self:PopulateScrollList(scrollFrame, {})
        return
    end

    local entries = self:FilterBySlot(selectedSlot)
    -- Sort by ilvl descending, stable by itemID
    table.sort(entries, function(a, b)
        if (a.ilvl or 0) ~= (b.ilvl or 0) then
            return (a.ilvl or 0) > (b.ilvl or 0)
        end
        return (a.itemID or 0) < (b.itemID or 0)
    end)
    self:PopulateScrollList(scrollFrame, entries)
end


--------------------------------------------------------------
-- LifaMpLoop  BrowseByStats.lua  –  Browse loot by stat priority
--------------------------------------------------------------

local statsFrame, scrollFrame
local enabledStats = {}   -- { HASTE = true, CRIT = true, ... }
local statOrder = {}       -- ordered list: { "HASTE", "CRIT", ... } for priority weighting
local selectedSlotFilter = nil  -- nil = all slots

local STAT_LIST = {
    { key = "HASTE",   internalKey = "ITEM_MOD_HASTE_RATING_SHORT",   label = "STAT_HASTE",       color = {1, 1, 0} },
    { key = "CRIT",    internalKey = "ITEM_MOD_CRIT_RATING_SHORT",    label = "STAT_CRIT",        color = {1, 0.27, 0.27} },
    { key = "MASTERY", internalKey = "ITEM_MOD_MASTERY_RATING_SHORT", label = "STAT_MASTERY",     color = {0.27, 1, 0.27} },
    { key = "VERS",    internalKey = "ITEM_MOD_VERSATILITY",          label = "STAT_VERSATILITY", color = {0.53, 0.53, 1} },
}

--------------------------------------------------------------
-- Build the tab UI
--------------------------------------------------------------
function LifaMpLoop:BuildStatsTab(parent)
    statsFrame = parent

    -- Top area: stat checkboxes + priority ordering
    local filterBar = CreateFrame("Frame", nil, parent)
    filterBar:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, -4)
    filterBar:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, -4)
    filterBar:SetHeight(110)

    local filterBg = filterBar:CreateTexture(nil, "BACKGROUND")
    filterBg:SetAllPoints()
    filterBg:SetColorTexture(0, 0, 0, 0.3)

    -- Slot dropdown label + button (top-right area)
    local slotLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    slotLabel:SetPoint("TOPRIGHT", filterBar, "TOPRIGHT", -130, -8)
    slotLabel:SetText(LPL["FILTER_SLOT"])

    local slotDropdown = CreateFrame("Button", nil, filterBar)
    slotDropdown:SetSize(120, 22)
    slotDropdown:SetPoint("LEFT", slotLabel, "RIGHT", 4, 0)

    slotDropdown.bg = slotDropdown:CreateTexture(nil, "BACKGROUND")
    slotDropdown.bg:SetAllPoints()
    slotDropdown.bg:SetColorTexture(0.15, 0.15, 0.15, 0.9)

    slotDropdown.border = slotDropdown:CreateTexture(nil, "BORDER")
    slotDropdown.border:SetPoint("TOPLEFT", -1, 1)
    slotDropdown.border:SetPoint("BOTTOMRIGHT", 1, -1)
    slotDropdown.border:SetColorTexture(0.4, 0.4, 0.4, 1)

    slotDropdown.text = slotDropdown:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    slotDropdown.text:SetPoint("CENTER")
    slotDropdown.text:SetText(LPL["ALL_SLOTS"])

    slotDropdown.arrow = slotDropdown:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    slotDropdown.arrow:SetPoint("RIGHT", slotDropdown, "RIGHT", -4, 0)
    slotDropdown.arrow:SetText("▼")

    -- Dropdown menu frame
    local slotMenu = CreateFrame("Frame", "LifaMpLoopSlotMenu", slotDropdown)
    slotMenu:SetFrameStrata("DIALOG")
    slotMenu:SetSize(120, 10)
    slotMenu:SetPoint("TOP", slotDropdown, "BOTTOM", 0, -1)
    slotMenu.bg = slotMenu:CreateTexture(nil, "BACKGROUND")
    slotMenu.bg:SetAllPoints()
    slotMenu.bg:SetColorTexture(0.1, 0.1, 0.1, 0.95)
    slotMenu:Hide()
    slotMenu:EnableMouse(true)

    -- Build slot menu items: "All" + each slot
    local slotOptions = { { key = nil, label = LPL["ALL_SLOTS"] } }
    for _, slotKey in ipairs(LifaMpLoop.SLOT_ORDER) do
        local locKey = LifaMpLoop.SLOT_LOCALE_KEY[slotKey]
        slotOptions[#slotOptions + 1] = { key = slotKey, label = LPL[locKey] or slotKey }
    end

    local menuButtons = {}
    for i, opt in ipairs(slotOptions) do
        local mbtn = CreateFrame("Button", nil, slotMenu)
        mbtn:SetSize(116, 18)
        mbtn:SetPoint("TOPLEFT", slotMenu, "TOPLEFT", 2, -(i - 1) * 18 - 2)
        mbtn.highlight = mbtn:CreateTexture(nil, "HIGHLIGHT")
        mbtn.highlight:SetAllPoints()
        mbtn.highlight:SetColorTexture(1, 1, 1, 0.1)
        mbtn.text = mbtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        mbtn.text:SetPoint("LEFT", 4, 0)
        mbtn.text:SetText(opt.label)
        mbtn:SetScript("OnClick", function()
            selectedSlotFilter = opt.key
            slotDropdown.text:SetText(opt.label)
            slotMenu:Hide()
            LifaMpLoop:RefreshStatsTab()
        end)
        menuButtons[i] = mbtn
    end
    slotMenu:SetHeight(#slotOptions * 18 + 4)

    slotDropdown:SetScript("OnClick", function()
        if slotMenu:IsShown() then
            slotMenu:Hide()
        else
            slotMenu:Show()
        end
    end)

    -- Hide menu when clicking elsewhere or switching tabs
    slotMenu:SetScript("OnShow", function(self)
        -- Register a global mouse handler to close on outside click
        self:SetScript("OnUpdate", function(self)
            if not self:IsMouseOver() and not slotDropdown:IsMouseOver() then
                if IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton") then
                    self:Hide()
                end
            end
        end)
    end)
    slotMenu:SetScript("OnHide", function(self)
        self:SetScript("OnUpdate", nil)
    end)

    filterBar.slotDropdown = slotDropdown

    -- Instruction label
    local instrLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    instrLabel:SetPoint("TOPLEFT", filterBar, "TOPLEFT", 8, -6)
    instrLabel:SetText(LPL["FILTER_STATS"])

    -- Stat checkboxes
    filterBar.checkboxes = {}
    for i, statInfo in ipairs(STAT_LIST) do
        local cb = CreateFrame("CheckButton", "LifaMpLoopStatCB" .. i, filterBar, "UICheckButtonTemplate")
        cb:SetSize(26, 26)
        cb:SetPoint("TOPLEFT", filterBar, "TOPLEFT", 8 + (i - 1) * 150, -24)
        cb:SetChecked(true)

        cb.text = cb:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        cb.text:SetPoint("LEFT", cb, "RIGHT", 2, 0)
        cb.text:SetText(LPL[statInfo.label] or statInfo.key)
        cb.text:SetTextColor(unpack(statInfo.color))

        enabledStats[statInfo.key] = true

        cb:SetScript("OnClick", function(self)
            enabledStats[statInfo.key] = self:GetChecked() and true or false
            LifaMpLoop:RefreshStatsTab()
        end)

        filterBar.checkboxes[i] = cb
    end

    -- Priority label
    local prioLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    prioLabel:SetPoint("TOPLEFT", filterBar, "TOPLEFT", 8, -58)
    prioLabel:SetText(LPL["STAT_PRIORITY"])

    -- Priority buttons (click to cycle position / simple numbered buttons)
    filterBar.prioButtons = {}
    statOrder = {}

    -- Initialize from saved priority or default
    local savedPrio = LifaMpLoop.db and LifaMpLoop.db.statPriority
    if savedPrio and #savedPrio > 0 then
        for _, internalKey in ipairs(savedPrio) do
            local friendlyKey = LifaMpLoop.STAT_KEYS[internalKey]
            if friendlyKey then
                statOrder[#statOrder + 1] = friendlyKey
            end
        end
    end
    -- Fill any missing stats
    for _, statInfo in ipairs(STAT_LIST) do
        local found = false
        for _, sk in ipairs(statOrder) do
            if sk == statInfo.key then found = true; break end
        end
        if not found then
            statOrder[#statOrder + 1] = statInfo.key
        end
    end

    local function RebuildPrioButtons()
        for _, btn in ipairs(filterBar.prioButtons) do
            btn:Hide()
        end

        for rank, friendlyKey in ipairs(statOrder) do
            local statInfo
            for _, si in ipairs(STAT_LIST) do
                if si.key == friendlyKey then statInfo = si; break end
            end
            if not statInfo then break end

            local btn = filterBar.prioButtons[rank]
            if not btn then
                btn = CreateFrame("Button", nil, filterBar)
                btn:SetSize(130, 20)
                btn.bg = btn:CreateTexture(nil, "BACKGROUND")
                btn.bg:SetAllPoints()
                btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                btn.text:SetPoint("CENTER")

                -- Left click = move up, Right click = move down
                btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
                btn:SetScript("OnClick", function(self, button)
                    local idx = self.rank
                    if button == "LeftButton" and idx > 1 then
                        statOrder[idx], statOrder[idx - 1] = statOrder[idx - 1], statOrder[idx]
                        LifaMpLoop:SaveStatPriority()
                        RebuildPrioButtons()
                        LifaMpLoop:RefreshStatsTab()
                    elseif button == "RightButton" and idx < #statOrder then
                        statOrder[idx], statOrder[idx + 1] = statOrder[idx + 1], statOrder[idx]
                        LifaMpLoop:SaveStatPriority()
                        RebuildPrioButtons()
                        LifaMpLoop:RefreshStatsTab()
                    end
                end)

                filterBar.prioButtons[rank] = btn
            end

            btn:SetPoint("TOPLEFT", filterBar, "TOPLEFT", 8 + (rank - 1) * 140, -78)
            btn.rank = rank
            btn.bg:SetColorTexture(statInfo.color[1], statInfo.color[2], statInfo.color[3], 0.25)
            btn.text:SetText(rank .. ". " .. (LPL[statInfo.label] or statInfo.key))
            btn.text:SetTextColor(statInfo.color[1], statInfo.color[2], statInfo.color[3])
            btn:Show()
        end
    end

    RebuildPrioButtons()
    filterBar.RebuildPrioButtons = RebuildPrioButtons

    statsFrame.filterBar = filterBar

    -- Item list area below filter
    local listArea = CreateFrame("Frame", nil, parent)
    listArea:SetPoint("TOPLEFT", filterBar, "BOTTOMLEFT", 0, -4)
    listArea:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)

    scrollFrame = LifaMpLoop:CreateScrollableList(listArea)
    statsFrame.scrollFrame = scrollFrame
end

--------------------------------------------------------------
-- Save stat priority to DB
--------------------------------------------------------------
function LifaMpLoop:SaveStatPriority()
    if not self.db then return end
    local prio = {}
    for _, friendlyKey in ipairs(statOrder) do
        local internalKey = LifaMpLoop.STAT_INTERNAL[friendlyKey]
        if internalKey then
            prio[#prio + 1] = internalKey
        end
    end
    self.db.statPriority = prio
end

--------------------------------------------------------------
-- Refresh
--------------------------------------------------------------
function LifaMpLoop:RefreshStatsTab()
    if not statsFrame or not statsFrame:IsShown() then return end

    if not self:IsDataReady() then
        self:PopulateScrollList(scrollFrame, {})
        return
    end

    -- Collect enabled stat friendly keys
    local activeStats = {}
    for _, friendlyKey in ipairs(statOrder) do
        if enabledStats[friendlyKey] then
            activeStats[#activeStats + 1] = friendlyKey
        end
    end

    if #activeStats == 0 then
        self:PopulateScrollList(scrollFrame, {})
        return
    end

    -- Filter items that have at least one of the enabled stats
    local entries = self:FilterByStats(activeStats)

    -- Apply slot filter
    if selectedSlotFilter then
        local filtered = {}
        for _, entry in ipairs(entries) do
            if entry.slotKey == selectedSlotFilter then
                filtered[#filtered + 1] = entry
            end
        end
        entries = filtered
    end

    -- Build priority list (only enabled stats, in priority order)
    local priorityInternal = {}
    for _, friendlyKey in ipairs(statOrder) do
        if enabledStats[friendlyKey] then
            local internalKey = LifaMpLoop.STAT_INTERNAL[friendlyKey]
            if internalKey then
                priorityInternal[#priorityInternal + 1] = internalKey
            end
        end
    end

    -- Sort by stat weight
    self:SortByStatPriority(entries, priorityInternal)

    self:PopulateScrollList(scrollFrame, entries)
end


--------------------------------------------------------------
-- LifaMpLoop  BrowseByDungeon.lua  –  Browse loot by dungeon
--------------------------------------------------------------

local dungeonFrame, scrollFrame
local selectedInstanceID = nil

--------------------------------------------------------------
-- Build the tab UI
--------------------------------------------------------------
function LifaMpLoop:BuildDungeonTab(parent)
    dungeonFrame = parent

    -- Left: dungeon button list
    local sidebar = CreateFrame("Frame", nil, parent)
    sidebar:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
    sidebar:SetPoint("BOTTOMLEFT", parent, "BOTTOMLEFT", 0, 0)
    sidebar:SetWidth(160)

    local sidebarBg = sidebar:CreateTexture(nil, "BACKGROUND")
    sidebarBg:SetAllPoints()
    sidebarBg:SetColorTexture(0, 0, 0, 0.3)

    -- Scrollable sidebar for dungeons (in case there are many)
    local sideScroll = CreateFrame("ScrollFrame", nil, sidebar, "ScrollFrameTemplate")
    sideScroll:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 2, -2)
    sideScroll:SetPoint("BOTTOMRIGHT", sidebar, "BOTTOMRIGHT", -18, 2)

    local sideChild = CreateFrame("Frame", nil, sideScroll)
    sideChild:SetWidth(sideScroll:GetWidth() or 138)
    sideScroll:SetScrollChild(sideChild)

    sidebar.scrollFrame = sideScroll
    sidebar.scrollChild = sideChild
    sidebar.buttons = {}

    dungeonFrame.sidebar = sidebar

    -- Right: item scroll list
    local listArea = CreateFrame("Frame", nil, parent)
    listArea:SetPoint("TOPLEFT", sidebar, "TOPRIGHT", 4, 0)
    listArea:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)

    scrollFrame = LifaMpLoop:CreateScrollableList(listArea)
    dungeonFrame.scrollFrame = scrollFrame
end

--------------------------------------------------------------
-- Rebuild sidebar dungeon buttons
--------------------------------------------------------------
local function RebuildDungeonSidebar()
    local sidebar = dungeonFrame.sidebar
    if not sidebar then return end

    local scrollChild = sidebar.scrollChild
    for _, btn in ipairs(sidebar.buttons) do
        btn:Hide()
    end

    local dungeons = LifaMpLoop.dungeons or {}
    scrollChild:SetHeight(#dungeons * 30 + 4)

    for i, dg in ipairs(dungeons) do
        local btn = sidebar.buttons[i]
        if not btn then
            btn = CreateFrame("Button", nil, scrollChild)
            btn:SetSize(136, 28)

            btn.bg = btn:CreateTexture(nil, "BACKGROUND")
            btn.bg:SetAllPoints()
            btn.bg:SetColorTexture(0.2, 0.2, 0.2, 0.5)

            btn.highlight = btn:CreateTexture(nil, "HIGHLIGHT")
            btn.highlight:SetAllPoints()
            btn.highlight:SetColorTexture(1, 1, 1, 0.1)

            btn.selectedTex = btn:CreateTexture(nil, "BORDER")
            btn.selectedTex:SetAllPoints()
            btn.selectedTex:SetColorTexture(0, 0.5, 0.8, 0.3)
            btn.selectedTex:Hide()

            btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            btn.text:SetPoint("LEFT", btn, "LEFT", 6, 0)
            btn.text:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
            btn.text:SetJustifyH("LEFT")
            btn.text:SetWordWrap(false)

            btn:SetScript("OnClick", function(self)
                selectedInstanceID = self.instanceID
                LifaMpLoop:RefreshDungeonTab()
            end)

            sidebar.buttons[i] = btn
        end

        btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -(i - 1) * 30 - 2)
        btn.instanceID = dg.instanceID
        btn.text:SetText(dg.name)
        btn:Show()

        -- Select first dungeon by default
        if i == 1 and not selectedInstanceID then
            selectedInstanceID = dg.instanceID
        end
    end
end

--------------------------------------------------------------
-- Refresh
--------------------------------------------------------------
function LifaMpLoop:RefreshDungeonTab()
    if not dungeonFrame or not dungeonFrame:IsShown() then return end

    RebuildDungeonSidebar()

    -- Update sidebar highlights
    if dungeonFrame.sidebar then
        for _, btn in ipairs(dungeonFrame.sidebar.buttons) do
            if btn.instanceID == selectedInstanceID then
                btn.selectedTex:Show()
            else
                btn.selectedTex:Hide()
            end
        end
    end

    if not self:IsDataReady() or not selectedInstanceID then
        self:PopulateScrollList(scrollFrame, {})
        return
    end

    local entries = self:FilterByDungeon(selectedInstanceID)

    -- Group by boss name for display: sort by bossName then itemID
    table.sort(entries, function(a, b)
        if (a.bossName or "") ~= (b.bossName or "") then
            return (a.bossName or "") < (b.bossName or "")
        end
        return (a.itemID or 0) < (b.itemID or 0)
    end)

    self:PopulateScrollList(scrollFrame, entries)
end

--------------------------------------------------------------
-- LifaMpLoop  Watchlist.lua  –  Watched items grouped by dungeon
--------------------------------------------------------------

local watchFrame
local collapsedDungeons = {}  -- { [dungeonName] = true/false }
local ITEM_ROW_HEIGHT = 42
local HEADER_HEIGHT = 28

--------------------------------------------------------------
-- Build the tab UI
--------------------------------------------------------------
function LifaMpLoop:BuildWatchlistTab(parent)
    watchFrame = parent

    -- Top bar: auto-whisper toggle
    local topBar = CreateFrame("Frame", nil, parent)
    topBar:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, -4)
    topBar:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, -4)
    topBar:SetHeight(100)

    local whisperCB = CreateFrame("CheckButton", "LifaMpLoopWhisperCB", topBar, "UICheckButtonTemplate")
    whisperCB:SetSize(26, 26)
    whisperCB:SetPoint("TOPLEFT", topBar, "TOPLEFT", 4, 0)
    whisperCB:SetChecked(LifaMpLoop.db and LifaMpLoop.db.autoWhisper ~= false)
    whisperCB.text = whisperCB:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    whisperCB.text:SetPoint("LEFT", whisperCB, "RIGHT", 2, 0)
    whisperCB.text:SetText(LPL["AUTO_WHISPER"])
    whisperCB:SetScript("OnClick", function(self)
        if LifaMpLoop.db then
            LifaMpLoop.db.autoWhisper = self:GetChecked() and true or false
        end
    end)
    watchFrame.whisperCB = whisperCB

    -- Whisper template label
    local templateLabel = topBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    templateLabel:SetPoint("TOPLEFT", whisperCB, "BOTTOMLEFT", 2, -4)
    templateLabel:SetText(LPL["WHISPER_TEMPLATE_LABEL"])
    templateLabel:SetTextColor(0.8, 0.8, 0.6)

    -- Multi-line whisper template EditBox with background
    local boxBg = CreateFrame("Frame", nil, topBar, "BackdropTemplate")
    boxBg:SetPoint("TOPLEFT", templateLabel, "BOTTOMLEFT", 0, -2)
    boxBg:SetPoint("RIGHT", topBar, "RIGHT", -8, 0)
    boxBg:SetHeight(50)
    boxBg:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    boxBg:SetBackdropColor(0, 0, 0, 0.5)
    boxBg:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)

    local boxScroll = CreateFrame("ScrollFrame", nil, boxBg)
    boxScroll:SetPoint("TOPLEFT", boxBg, "TOPLEFT", 6, -4)
    boxScroll:SetPoint("BOTTOMRIGHT", boxBg, "BOTTOMRIGHT", -6, 4)

    local templateBox = CreateFrame("EditBox", nil, boxScroll)
    templateBox:SetWidth(boxScroll:GetWidth() or 600)
    templateBox:SetMultiLine(true)
    templateBox:SetAutoFocus(false)
    templateBox:SetMaxLetters(255)
    templateBox:SetFontObject(ChatFontNormal)
    boxScroll:SetScrollChild(templateBox)

    local savedTpl = LifaMpLoop.db and LifaMpLoop.db.whisperTemplate or ""
    templateBox:SetText(savedTpl ~= "" and savedTpl or LPL["LOOT_WHISPER_MSG"])
    templateBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    templateBox:SetScript("OnEditFocusLost", function(self)
        if LifaMpLoop.db then
            local text = self:GetText() or ""
            if text == LPL["LOOT_WHISPER_MSG"] then
                LifaMpLoop.db.whisperTemplate = ""
            else
                LifaMpLoop.db.whisperTemplate = text
            end
        end
    end)
    -- Update width when parent resizes
    boxScroll:SetScript("OnSizeChanged", function(self, w)
        templateBox:SetWidth(w)
    end)
    watchFrame.templateBox = templateBox

    -- Full-area scroll frame
    local scrollFrame = CreateFrame("ScrollFrame", nil, parent, "ScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", topBar, "BOTTOMLEFT", 0, -2)
    scrollFrame:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", -24, 4)

    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetWidth(scrollFrame:GetWidth() or 680)
    scrollFrame:SetScrollChild(scrollChild)

    watchFrame.scrollFrame = scrollFrame
    watchFrame.scrollChild = scrollChild
    watchFrame.headers = {}
    watchFrame.rows = {}
end

--------------------------------------------------------------
-- Create a dungeon group header
--------------------------------------------------------------
local function GetOrCreateHeader(index)
    local sc = watchFrame.scrollChild
    if watchFrame.headers[index] then
        return watchFrame.headers[index]
    end

    local header = CreateFrame("Button", nil, sc)
    header:SetHeight(HEADER_HEIGHT)

    header.bg = header:CreateTexture(nil, "BACKGROUND")
    header.bg:SetAllPoints()
    header.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)

    header.highlight = header:CreateTexture(nil, "HIGHLIGHT")
    header.highlight:SetAllPoints()
    header.highlight:SetColorTexture(1, 1, 1, 0.05)

    header.arrow = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    header.arrow:SetPoint("LEFT", header, "LEFT", 8, 0)
    header.arrow:SetWidth(16)

    header.text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    header.text:SetPoint("LEFT", header.arrow, "RIGHT", 4, 0)
    header.text:SetJustifyH("LEFT")
    header.text:SetTextColor(0, 0.8, 1)

    header.countText = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    header.countText:SetPoint("RIGHT", header, "RIGHT", -8, 0)
    header.countText:SetTextColor(0.6, 0.6, 0.6)

    header:SetScript("OnClick", function(self)
        local dName = self.dungeonName
        collapsedDungeons[dName] = not collapsedDungeons[dName]
        LifaMpLoop:RefreshWatchlistTab()
    end)

    watchFrame.headers[index] = header
    return header
end

--------------------------------------------------------------
-- Refresh
--------------------------------------------------------------
function LifaMpLoop:RefreshWatchlistTab()
    if not watchFrame or not watchFrame:IsShown() then return end

    local sc = watchFrame.scrollChild
    local watchlist = self.db and self.db.watchlist or {}

    -- Hide everything
    for _, h in ipairs(watchFrame.headers) do h:Hide() end
    for _, r in ipairs(watchFrame.rows) do r:Hide() end

    -- Check empty
    local isEmpty = true
    for _ in pairs(watchlist) do isEmpty = false; break end

    if isEmpty then
        if not watchFrame.emptyText then
            watchFrame.emptyText = sc:CreateFontString(nil, "OVERLAY", "GameFontDisableLarge")
            watchFrame.emptyText:SetPoint("CENTER", sc, "CENTER", 0, 0)
        end
        watchFrame.emptyText:SetText(LPL["WATCHLIST_EMPTY"])
        watchFrame.emptyText:Show()
        sc:SetHeight(200)
        return
    end

    if watchFrame.emptyText then watchFrame.emptyText:Hide() end

    -- Group watched items by dungeon
    local dungeonGroups = {}  -- { { name, entries={} }, ... }
    local dungeonIndex = {}   -- { dungeonName → index in dungeonGroups }

    for itemID, info in pairs(watchlist) do
        local dName = info.dungeonName or "?"
        -- Find full loot entry from allLoot (skip if not in current spec's data)
        local entry = nil
        for _, lootEntry in ipairs(self.allLoot) do
            if lootEntry.itemID == itemID then
                entry = lootEntry
                break
            end
        end
        if entry then
            if not dungeonIndex[dName] then
                dungeonGroups[#dungeonGroups + 1] = { name = dName, entries = {} }
                dungeonIndex[dName] = #dungeonGroups
            end
            local g = dungeonGroups[dungeonIndex[dName]]
            g.entries[#g.entries + 1] = entry
        end
    end

    -- Sort dungeon groups by name
    table.sort(dungeonGroups, function(a, b) return a.name < b.name end)

    -- Sort items within each group by boss
    for _, g in ipairs(dungeonGroups) do
        table.sort(g.entries, function(a, b)
            return (a.bossName or "") < (b.bossName or "")
        end)
    end

    -- Layout: headers + rows
    local yOffset = 0
    local headerIdx = 0
    local rowIdx = 0
    local width = watchFrame.scrollFrame:GetWidth() or 680

    for _, group in ipairs(dungeonGroups) do
        headerIdx = headerIdx + 1
        local header = GetOrCreateHeader(headerIdx)
        header:SetPoint("TOPLEFT", sc, "TOPLEFT", 0, -yOffset)
        header:SetPoint("RIGHT", sc, "RIGHT", 0, 0)
        header.dungeonName = group.name

        local collapsed = collapsedDungeons[group.name]
        header.arrow:SetText(collapsed and "+" or "-")
        header.text:SetText(group.name)
        header.countText:SetText(#group.entries .. " items")
        header:Show()

        yOffset = yOffset + HEADER_HEIGHT

        if not collapsed then
            for _, entry in ipairs(group.entries) do
                rowIdx = rowIdx + 1
                if not watchFrame.rows[rowIdx] then
                    watchFrame.rows[rowIdx] = self:CreateItemRow(sc, rowIdx)
                end
                local row = watchFrame.rows[rowIdx]
                row:SetPoint("TOPLEFT", sc, "TOPLEFT", 12, -yOffset)
                row:SetPoint("RIGHT", sc, "RIGHT", 0, 0)
                self:SetItemRowData(row, entry)
                yOffset = yOffset + ITEM_ROW_HEIGHT
            end
        end
    end

    sc:SetWidth(width)
    sc:SetHeight(yOffset + 10)
end

--------------------------------------------------------------
-- LifaMpLoop  LootMonitor.lua  –  Monitor loot events
--
-- 1. When the player loots a watched item → auto-remove from watchlist
-- 2. When a party member loots a watched item after dungeon completion
--    → auto-whisper them politely to ask for trade
--------------------------------------------------------------

local monitorFrame = CreateFrame("Frame")

-- Anti-spam: per-player cooldown
local playerLastWhisper = {} -- { ["playerName"] = timestamp }
local PLAYER_COOLDOWN = 60  -- same player: 60 seconds

--------------------------------------------------------------
-- Extract itemID from an item link string
--------------------------------------------------------------
local function GetItemIDFromLink(link)
    if not link then return nil end
    local id = link:match("item:(%d+)")
    return id and tonumber(id) or nil
end

--------------------------------------------------------------
-- Check if an itemID is on our watchlist
--------------------------------------------------------------
local function IsWatchedItem(itemID)
    return LifaMpLoop.db and LifaMpLoop.db.watchlist and LifaMpLoop.db.watchlist[itemID] ~= nil
end

--------------------------------------------------------------
-- Handle: player received a watched item
--------------------------------------------------------------
local function OnPlayerLootedWatchedItem(itemID, itemLink)
    LifaMpLoop:RemoveFromWatchlist(itemID)
    local msg = string.format(LPL["ITEM_OBTAINED"], itemLink or itemID)
    print("|cff00ccff[Lifa M+ Loop]|r " .. msg)
end

--------------------------------------------------------------
-- Handle: party member received a watched item
--------------------------------------------------------------
local function OnPartyMemberLootedWatchedItem(playerName, itemID, itemLink)
    if not playerName or playerName == "" then return end

    -- Check if auto-whisper is enabled
    if LifaMpLoop.db and LifaMpLoop.db.autoWhisper == false then return end

    -- Only whisper in 5-man party (not raid, not solo)
    if not IsInGroup() or IsInRaid() then
        return
    end

    -- Don't whisper ourselves
    local myName = UnitName("player")
    local shortName = playerName:match("^([^%-]+)")
    if shortName == myName then return end

    -- Per-player cooldown: 60 seconds
    local now = GetTime()
    if playerLastWhisper[playerName] and (now - playerLastWhisper[playerName]) < PLAYER_COOLDOWN then
        return
    end
    playerLastWhisper[playerName] = now

    -- Send whisper
    local template = (LifaMpLoop.db.whisperTemplate and LifaMpLoop.db.whisperTemplate ~= "") and LifaMpLoop.db.whisperTemplate or LPL["LOOT_WHISPER_MSG"]
    local whisperMsg = string.format(template, itemLink or ("[item:" .. itemID .. "]"))
    SendChatMessage(whisperMsg, "WHISPER", nil, playerName)

    local notice = string.format(LPL["WHISPER_SENT"], playerName)
    print("|cff00ccff[Lifa M+ Loop]|r " .. notice)
end

--------------------------------------------------------------
-- CHAT_MSG_LOOT handler
-- Fires for all loot in the group
-- arg1 = loot message string, e.g. "PlayerName receives loot: [Item Link]."
--------------------------------------------------------------
local function OnChatMsgLoot(msg)
    if not LifaMpLoop.db then return end

    -- Extract item link from the message
    -- WoW 12.0 uses |cnXXXX:|Hitem:...|h[Name]|h|r  (new format)
    -- Older versions use |cxxxxxxxx|Hitem:...|h[Name]|h|r
    local itemLink = msg:match("|c%x+|Hitem:.-|h%[.-%]|h|r") or msg:match("|cn[^:]+:|Hitem:.-|h%[.-%]|h|r")
    if not itemLink then return end

    local itemID = GetItemIDFromLink(itemLink)
    if not itemID then return end

    local watched = IsWatchedItem(itemID)

    -- Determine who looted it
    local selfPattern = LOOT_ITEM_SELF:gsub("%%s", ".+")
    local isSelf = msg:match(selfPattern)
    local looter = "SELF"

    if not isSelf then
        local namePattern = LOOT_ITEM:gsub("%%s", "(.+)", 1):gsub("%%s", ".+")
        looter = msg:match(namePattern) or "UNKNOWN"
    end

    -- Test mode: one-line summary
    if LifaMpLoop.testMode then
        print(string.format("|cffFFFF00[Test]|r %s (ID:%d) | %s | Looter: %s",
            itemLink, itemID,
            watched and "|cff00ff00Watched|r" or "|cff888888Not watched|r",
            looter))
    end

    if isSelf then
        if watched then
            OnPlayerLootedWatchedItem(itemID, itemLink)
        end
    else
        local playerName = (looter ~= "UNKNOWN") and looter or nil
        if playerName then
            if watched or LifaMpLoop.testMode then
                OnPartyMemberLootedWatchedItem(playerName, itemID, itemLink)
            end
        end
    end
end

--------------------------------------------------------------
-- BOSS_KILL / ENCOUNTER_END as additional context
-- We track whether we just finished a dungeon boss
--------------------------------------------------------------
local recentBossKill = false
local bossKillTimer = nil

local function OnBossKill()
    recentBossKill = true
    -- Reset after 60 seconds (loot window period)
    if bossKillTimer then
        -- Cancel is not available for C_Timer, so just let it expire
    end
    C_Timer.After(60, function()
        recentBossKill = false
    end)
end

--------------------------------------------------------------
-- Init
--------------------------------------------------------------
function LifaMpLoop:InitLootMonitor()
    monitorFrame:RegisterEvent("CHAT_MSG_LOOT")
    monitorFrame:RegisterEvent("BOSS_KILL")
    monitorFrame:RegisterEvent("ENCOUNTER_END")

    monitorFrame:SetScript("OnEvent", function(self, event, ...)
        if event == "CHAT_MSG_LOOT" then
            local msg = ...
            if msg and not issecretvalue(msg) then
                OnChatMsgLoot(msg)
            end
        elseif event == "BOSS_KILL" or event == "ENCOUNTER_END" then
            OnBossKill()
        end
    end)
end
