--[[
============================================================================
ETB - SourcesSummaryPanel.lua
============================================================================
Panel that summarizes all sources for items in the Live Preview.
Groups items by source type (raids, dungeons, vendors, etc.) and shows
collection status with route planning for efficient farming.
============================================================================
]]--

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

ETB.SourcesSummaryPanel = {
    frame = nil,
    currentItems = {},
    processedSources = {},
    showCollected = true,
    groupFrames = {},
    itemFrames = {},
    subgroupFrames = {},
}

local Panel = ETB.SourcesSummaryPanel

-- =============================================================================
-- CONSTANTS
-- =============================================================================

local PANEL_WIDTH = 500
local PANEL_HEIGHT = 600
local ITEM_HEIGHT = 32
local GROUP_HEADER_HEIGHT = 26
local SUBGROUP_HEADER_HEIGHT = 22

-- Source type display order and info
local SOURCE_TYPE_ORDER = {
    { type = "RAID", name = "Raids", icon = "Interface\\Icons\\INV_Helmet_06", color = "|cffff8000" },
    { type = "DUNGEON", name = "Dungeons", icon = "Interface\\Icons\\INV_Misc_Key_10", color = "|cff0070dd" },
    { type = "WORLD_BOSS", name = "World Bosses", icon = "Interface\\Icons\\Ability_Creature_Cursed_02", color = "|cffa335ee" },
    { type = "VENDOR", name = "Vendors", icon = "Interface\\Icons\\INV_Misc_Coin_01", color = "|cffffffff" },
    { type = "QUEST", name = "Quests", icon = "Interface\\Icons\\INV_Misc_QuestionMark", color = "|cffffff00" },
    { type = "WORLD_DROP", name = "World Drops", icon = "Interface\\Icons\\INV_Misc_Bag_10", color = "|cff1eff00" },
    { type = "CRAFTED", name = "Crafted", icon = "Interface\\Icons\\Trade_BlackSmithing", color = "|cffff7c0a" },
    { type = "PVP", name = "PvP", icon = "Interface\\Icons\\INV_BannerPVP_02", color = "|cffff0000" },
    { type = "ACHIEVEMENT", name = "Achievements", icon = "Interface\\Icons\\Achievement_General", color = "|cffffff00" },
    { type = "UNKNOWN", name = "Unknown Sources", icon = "Interface\\Icons\\INV_Misc_QuestionMark", color = "|cff808080" },
}

-- Zone routing order by continent/expansion for efficient farming
-- Lower order = visit first
local ZONE_ROUTE_ORDER = {
    -- Classic Eastern Kingdoms (south to north)
    ["Stranglethorn Vale"] = { continent = "EK", order = 1 },
    ["Westfall"] = { continent = "EK", order = 2 },
    ["Elwynn Forest"] = { continent = "EK", order = 3 },
    ["Redridge Mountains"] = { continent = "EK", order = 4 },
    ["Burning Steppes"] = { continent = "EK", order = 5 },
    ["Searing Gorge"] = { continent = "EK", order = 6 },
    ["Blackrock Mountain"] = { continent = "EK", order = 7 },
    ["Loch Modan"] = { continent = "EK", order = 8 },
    ["Wetlands"] = { continent = "EK", order = 9 },
    ["Arathi Highlands"] = { continent = "EK", order = 10 },
    ["Hillsbrad Foothills"] = { continent = "EK", order = 11 },
    ["Western Plaguelands"] = { continent = "EK", order = 12 },
    ["Eastern Plaguelands"] = { continent = "EK", order = 13 },
    
    -- Classic Kalimdor (south to north)
    ["Tanaris"] = { continent = "KAL", order = 1 },
    ["Un'Goro Crater"] = { continent = "KAL", order = 2 },
    ["Silithus"] = { continent = "KAL", order = 3 },
    ["Feralas"] = { continent = "KAL", order = 4 },
    ["Thousand Needles"] = { continent = "KAL", order = 5 },
    ["Desolace"] = { continent = "KAL", order = 6 },
    ["Dustwallow Marsh"] = { continent = "KAL", order = 7 },
    ["The Barrens"] = { continent = "KAL", order = 8 },
    ["Ashenvale"] = { continent = "KAL", order = 9 },
    ["Felwood"] = { continent = "KAL", order = 10 },
    ["Winterspring"] = { continent = "KAL", order = 11 },
    
    -- TBC Outland
    ["Hellfire Peninsula"] = { continent = "OUT", order = 1 },
    ["Zangarmarsh"] = { continent = "OUT", order = 2 },
    ["Terokkar Forest"] = { continent = "OUT", order = 3 },
    ["Nagrand"] = { continent = "OUT", order = 4 },
    ["Blade's Edge Mountains"] = { continent = "OUT", order = 5 },
    ["Netherstorm"] = { continent = "OUT", order = 6 },
    ["Shadowmoon Valley"] = { continent = "OUT", order = 7 },
    
    -- Wrath Northrend
    ["Borean Tundra"] = { continent = "NR", order = 1 },
    ["Howling Fjord"] = { continent = "NR", order = 2 },
    ["Dragonblight"] = { continent = "NR", order = 3 },
    ["Grizzly Hills"] = { continent = "NR", order = 4 },
    ["Zul'Drak"] = { continent = "NR", order = 5 },
    ["Sholazar Basin"] = { continent = "NR", order = 6 },
    ["Storm Peaks"] = { continent = "NR", order = 7 },
    ["Icecrown"] = { continent = "NR", order = 8 },
    
    -- MoP Pandaria
    ["The Jade Forest"] = { continent = "PAN", order = 1 },
    ["Valley of the Four Winds"] = { continent = "PAN", order = 2 },
    ["Krasarang Wilds"] = { continent = "PAN", order = 3 },
    ["Kun-Lai Summit"] = { continent = "PAN", order = 4 },
    ["Townlong Steppes"] = { continent = "PAN", order = 5 },
    ["Dread Wastes"] = { continent = "PAN", order = 6 },
    ["Vale of Eternal Blossoms"] = { continent = "PAN", order = 7 },
    ["Isle of Thunder"] = { continent = "PAN", order = 8 },
    ["Timeless Isle"] = { continent = "PAN", order = 9 },
}

local CONTINENT_ORDER = { "EK", "KAL", "OUT", "NR", "PAN" }
local CONTINENT_NAMES = {
    EK = "Eastern Kingdoms",
    KAL = "Kalimdor",
    OUT = "Outland",
    NR = "Northrend",
    PAN = "Pandaria",
}

-- =============================================================================
-- CREATE PANEL
-- =============================================================================

function Panel:Create()
    if self.frame then return self.frame end
    
    local f = CreateFrame("Frame", "ETBSourcesSummaryPanel", UIParent, "BackdropTemplate")
    f:SetSize(PANEL_WIDTH, PANEL_HEIGHT)
    -- Position to the RIGHT of the main frame, not overlapping
    f:SetPoint("LEFT", UIParent, "CENTER", 350, 0)
    f:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left = 8, right = 8, top = 8, bottom = 8 }
    })
    f:SetBackdropColor(0, 0, 0, 0.95)
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetFrameStrata("DIALOG")
    f:SetFrameLevel(100)
    f:SetClampedToScreen(true)
    
    -- Title
    f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    f.title:SetPoint("TOP", 0, -12)
    f.title:SetText("|cff00ffffOutfit Sources Summary|r")
    
    -- Close button
    f.closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    f.closeBtn:SetPoint("TOPRIGHT", -3, -3)
    
    -- Stats summary area
    f.statsFrame = CreateFrame("Frame", nil, f, "BackdropTemplate")
    f.statsFrame:SetSize(PANEL_WIDTH - 30, 50)
    f.statsFrame:SetPoint("TOP", 0, -38)
    f.statsFrame:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    f.statsFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
    
    -- Collection progress
    f.progressText = f.statsFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.progressText:SetPoint("TOPLEFT", 8, -8)
    f.progressText:SetText("0/0 items collected (0%)")
    
    -- Location summary
    f.locationText = f.statsFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.locationText:SetPoint("TOPLEFT", 8, -24)
    f.locationText:SetTextColor(0.8, 0.8, 0.8)
    f.locationText:SetText("Locations to visit: calculating...")
    
    -- Uncollected count (emphasized)
    f.uncollectedText = f.statsFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.uncollectedText:SetPoint("TOPRIGHT", -8, -8)
    f.uncollectedText:SetTextColor(1, 0.5, 0)
    f.uncollectedText:SetText("")
    
    -- Filter buttons
    f.filterFrame = CreateFrame("Frame", nil, f)
    f.filterFrame:SetSize(PANEL_WIDTH - 30, 24)
    f.filterFrame:SetPoint("TOP", f.statsFrame, "BOTTOM", 0, -4)
    
    f.showAllBtn = CreateFrame("Button", nil, f.filterFrame, "UIPanelButtonTemplate")
    f.showAllBtn:SetSize(100, 20)
    f.showAllBtn:SetPoint("LEFT", 0, 0)
    f.showAllBtn:SetText("Show All")
    f.showAllBtn:SetScript("OnClick", function()
        Panel.showCollected = true
        Panel:Refresh()
    end)
    
    f.uncollectedOnlyBtn = CreateFrame("Button", nil, f.filterFrame, "UIPanelButtonTemplate")
    f.uncollectedOnlyBtn:SetSize(120, 20)
    f.uncollectedOnlyBtn:SetPoint("LEFT", f.showAllBtn, "RIGHT", 5, 0)
    f.uncollectedOnlyBtn:SetText("Uncollected Only")
    f.uncollectedOnlyBtn:SetScript("OnClick", function()
        Panel.showCollected = false
        Panel:Refresh()
    end)
    
    -- View mode toggle (Sources vs Route)
    f.viewModeBtn = CreateFrame("Button", nil, f.filterFrame, "UIPanelButtonTemplate")
    f.viewModeBtn:SetSize(100, 20)
    f.viewModeBtn:SetPoint("RIGHT", 0, 0)
    f.viewModeBtn:SetText("Farm Route")
    f.viewModeBtn:SetScript("OnClick", function()
        Panel.viewMode = Panel.viewMode == "sources" and "route" or "sources"
        f.viewModeBtn:SetText(Panel.viewMode == "sources" and "Farm Route" or "By Source")
        Panel:Refresh()
    end)
    Panel.viewMode = "sources"
    
    -- Scroll frame for content
    f.scrollContainer = CreateFrame("Frame", nil, f, "BackdropTemplate")
    f.scrollContainer:SetPoint("TOPLEFT", 12, -125)
    f.scrollContainer:SetPoint("BOTTOMRIGHT", -12, 12)
    f.scrollContainer:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    f.scrollContainer:SetBackdropColor(0.02, 0.02, 0.02, 0.95)
    
    f.scrollFrame = CreateFrame("ScrollFrame", nil, f.scrollContainer, "UIPanelScrollFrameTemplate")
    f.scrollFrame:SetPoint("TOPLEFT", 4, -4)
    f.scrollFrame:SetPoint("BOTTOMRIGHT", -24, 4)
    
    f.scrollChild = CreateFrame("Frame", nil, f.scrollFrame)
    f.scrollChild:SetSize(f.scrollFrame:GetWidth(), 1)
    f.scrollFrame:SetScrollChild(f.scrollChild)
    
    -- Empty state message
    f.emptyText = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.emptyText:SetPoint("CENTER", f.scrollContainer, "CENTER", 0, 0)
    f.emptyText:SetText("|cff888888Add items to Live Preview\nto see sources|r")
    f.emptyText:Hide()
    
    -- ESC closes panel. v14.10.1: see Construction.lua for rationale.
    if UISpecialFrames then
        local already = false
        for _, name in ipairs(UISpecialFrames) do
            if name == "ETBSourcesSummaryPanel" then already = true; break end
        end
        if not already then tinsert(UISpecialFrames, "ETBSourcesSummaryPanel") end
    end
    f:Hide()
    
    self.frame = f
    return f
end

-- =============================================================================
-- SHOW / HIDE / REFRESH
-- =============================================================================

function Panel:Show(livePreviewSlots)
    if not self.frame then self:Create() end
    
    self.currentItems = livePreviewSlots or {}
    self:Refresh()
    self.frame:Show()
end

function Panel:Refresh()
    self:ProcessSources()
    if self.viewMode == "route" then
        self:PopulateFarmingRoute()
    else
        self:PopulateContent()
    end
    self:UpdateStats()
    self:UpdateFilterButtons()
end

function Panel:Hide()
    if self.frame then self.frame:Hide() end
end

function Panel:Toggle(livePreviewSlots)
    if self.frame and self.frame:IsShown() then
        self:Hide()
    else
        self:Show(livePreviewSlots)
    end
end

-- =============================================================================
-- PROCESS SOURCES
-- =============================================================================

function Panel:ProcessSources()
    self.processedSources = {}
    
    local sourceGroups = {}
    for _, typeInfo in ipairs(SOURCE_TYPE_ORDER) do
        sourceGroups[typeInfo.type] = {
            type = typeInfo.type,
            name = typeInfo.name,
            icon = typeInfo.icon,
            color = typeInfo.color,
            locations = {},
            items = {},
            totalItems = 0,
            collectedItems = 0,
        }
    end
    
    -- Process each item in the live preview
    for slotName, item in pairs(self.currentItems) do
        if item and item.itemID then
            local sourceInfo = self:GetItemSourceInfo(item)
            local groupType = sourceInfo.groupType or "UNKNOWN"
            local group = sourceGroups[groupType]
            
            if group then
                local itemEntry = {
                    item = item,
                    slotName = slotName,
                    sourceInfo = sourceInfo,
                    isCollected = item.isCollected,
                }
                
                group.totalItems = group.totalItems + 1
                if item.isCollected then
                    group.collectedItems = group.collectedItems + 1
                end
                
                -- Group by location
                local locationKey = sourceInfo.location or "Unknown Location"
                if not group.locations[locationKey] then
                    group.locations[locationKey] = {
                        name = locationKey,
                        instanceID = sourceInfo.instanceID,
                        items = {},
                        totalItems = 0,
                        collectedItems = 0,
                        bosses = {},
                    }
                end
                
                local location = group.locations[locationKey]
                location.totalItems = location.totalItems + 1
                if item.isCollected then
                    location.collectedItems = location.collectedItems + 1
                end
                
                -- For raids/dungeons, group by boss
                if sourceInfo.bossName and (groupType == "RAID" or groupType == "DUNGEON") then
                    local bossKey = sourceInfo.bossName
                    if not location.bosses[bossKey] then
                        location.bosses[bossKey] = {
                            name = bossKey,
                            encounterID = sourceInfo.encounterID,
                            order = sourceInfo.bossOrder or 999,
                            items = {},
                        }
                    end
                    table.insert(location.bosses[bossKey].items, itemEntry)
                else
                    table.insert(location.items, itemEntry)
                end
                
                table.insert(group.items, itemEntry)
            end
        end
    end
    
    self.processedSources = sourceGroups
end

function Panel:GetItemSourceInfo(item)
    local info = {
        groupType = "UNKNOWN",
        location = nil,
        bossName = nil,
        bossOrder = 999,
        instanceID = nil,
        encounterID = nil,
        sourceName = nil,
        sourceDetails = nil,
    }
    
    -- Try SourceMapper first
    if ETB.SourceMapper and ETB.SourceMapper.initialized then
        local source = ETB.SourceMapper:GetPrimarySource(item.itemID)
        if source then
            local sourceType = source.sourceType
            if sourceType == 1 then  -- Boss drop
                -- Determine if raid or dungeon (try mapper data first, then InstanceData)
                local mapperSource = ETB.SourceData_ATT and ETB.SourceData_ATT:GetPrimarySource(item.itemID) or nil
                
                if source.isWorldBoss or (mapperSource and mapperSource.isWorldBoss) then
                    info.groupType = "WORLD_BOSS"
                elseif mapperSource and mapperSource.isRaid then
                    info.groupType = "RAID"
                elseif source.instanceID and ETB.InstanceData then
                    local instanceInfo = ETB.InstanceData:GetInstance(source.instanceID)
                    if instanceInfo then
                        if instanceInfo.type == 2 then
                            info.groupType = "RAID"
                        else
                            info.groupType = "DUNGEON"
                        end
                    end
                end
                
                -- Default to DUNGEON if we couldn't determine
                if info.groupType == "UNKNOWN" then
                    info.groupType = "DUNGEON"
                end
                
                info.bossName = source.sourceName
                info.location = source.sourceLocation
                info.instanceID = source.instanceID
                info.encounterID = source.encounterID
                
                -- Get boss order for proper sorting
                if source.encounterID and ETB.InstanceData then
                    local encounter = ETB.InstanceData:GetEncounter(source.encounterID)
                    if encounter then
                        info.bossOrder = encounter.order or 999
                    end
                end
            elseif sourceType == 2 then
                info.groupType = "QUEST"
                info.location = source.sourceLocation or "Various"
                info.sourceName = source.sourceName or "Quest"
            elseif sourceType == 3 then
                info.groupType = "VENDOR"
                info.location = source.sourceLocation or "Various"
                info.sourceName = source.sourceName or "Vendor"
            elseif sourceType == 4 then
                info.groupType = "WORLD_DROP"
                info.location = source.sourceLocation or "World"
                info.sourceName = "World Drop"
            elseif sourceType == 5 then
                info.groupType = "ACHIEVEMENT"
                info.location = "Achievements"
                info.sourceName = source.sourceName or "Achievement"
            elseif sourceType == 6 then
                info.groupType = "CRAFTED"
                info.location = source.sourceName or "Crafted"
                info.sourceName = source.sourceName or "Crafted"
            end
            
            return info
        end
    end
    
    -- Fallback to item properties
    if item.sourceType then
        local typeStr = tostring(item.sourceType):upper()
        if typeStr:find("RAID") then info.groupType = "RAID"
        elseif typeStr:find("DUNGEON") then info.groupType = "DUNGEON"
        elseif typeStr:find("VENDOR") then info.groupType = "VENDOR"
        elseif typeStr:find("QUEST") then info.groupType = "QUEST"
        elseif typeStr:find("WORLD") then info.groupType = "WORLD_DROP"
        elseif typeStr:find("CRAFT") then info.groupType = "CRAFTED"
        elseif typeStr:find("PVP") then info.groupType = "PVP"
        end
    end
    
    info.location = item.sourceZone or item.dropSource or "Unknown"
    info.sourceName = item.dropSource or "Unknown"
    
    return info
end

-- =============================================================================
-- POPULATE CONTENT (By Source Type)
-- =============================================================================

function Panel:PopulateContent()
    local scrollChild = self.frame.scrollChild
    
    -- Hide all existing frames
    self:HideAllFrames()
    
    -- Check if there are any items
    local totalItems = 0
    for _, group in pairs(self.processedSources) do
        totalItems = totalItems + group.totalItems
    end
    
    if totalItems == 0 then
        self.frame.emptyText:Show()
        scrollChild:SetHeight(100)
        return
    end
    
    self.frame.emptyText:Hide()
    
    local yOffset = 5
    local groupIndex = 0
    local itemIndex = 0
    
    for _, typeInfo in ipairs(SOURCE_TYPE_ORDER) do
        local group = self.processedSources[typeInfo.type]
        if group and group.totalItems > 0 then
            -- Skip if filtering uncollected and all items collected
            local hasUncollected = (group.totalItems - group.collectedItems) > 0
            if self.showCollected or hasUncollected then
                groupIndex = groupIndex + 1
                
                -- Group header
                local groupFrame = self:GetOrCreateGroupFrame(groupIndex)
                groupFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -yOffset)
                groupFrame:SetSize(scrollChild:GetWidth() - 5, GROUP_HEADER_HEIGHT)
                
                local uncollectedCount = group.totalItems - group.collectedItems
                local headerText = string.format("|T%s:14:14|t %s%s|r  |cff888888(%d/%d)|r",
                    group.icon, group.color, group.name,
                    group.collectedItems, group.totalItems)
                if uncollectedCount > 0 then
                    headerText = headerText .. string.format(" |cffff8800[%d needed]|r", uncollectedCount)
                end
                groupFrame.text:SetText(headerText)
                groupFrame:Show()
                
                yOffset = yOffset + GROUP_HEADER_HEIGHT + 3
                
                -- Sort locations
                local sortedLocations = {}
                for locName, locData in pairs(group.locations) do
                    table.insert(sortedLocations, locData)
                end
                table.sort(sortedLocations, function(a, b) return a.name < b.name end)
                
                -- Render locations
                for _, location in ipairs(sortedLocations) do
                    local locHasUncollected = (location.totalItems - location.collectedItems) > 0
                    if self.showCollected or locHasUncollected then
                        -- Location subheader
                        local subgroupKey = groupIndex .. "_" .. location.name
                        local locHeaderFrame = self:GetOrCreateSubgroupFrame(subgroupKey)
                        locHeaderFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 8, -yOffset)
                        locHeaderFrame:SetSize(scrollChild:GetWidth() - 15, SUBGROUP_HEADER_HEIGHT)
                        
                        local locStatus = location.collectedItems == location.totalItems and "|cff00ff00DONE|r" or ""
                        local locHeaderText = string.format("|cff00ffff%s|r |cff666666(%d/%d)|r %s",
                            location.name, location.collectedItems, location.totalItems, locStatus)
                        locHeaderFrame.text:SetText(locHeaderText)
                        locHeaderFrame:Show()
                        
                        yOffset = yOffset + SUBGROUP_HEADER_HEIGHT + 2
                        
                        -- Render boss items (sorted by boss order)
                        if next(location.bosses) then
                            local sortedBosses = {}
                            for _, bossData in pairs(location.bosses) do
                                table.insert(sortedBosses, bossData)
                            end
                            table.sort(sortedBosses, function(a, b)
                                return (a.order or 999) < (b.order or 999)
                            end)
                            
                            for _, boss in ipairs(sortedBosses) do
                                for _, itemEntry in ipairs(boss.items) do
                                    if self.showCollected or not itemEntry.isCollected then
                                        itemIndex = itemIndex + 1
                                        local itemFrame = self:GetOrCreateItemFrame(itemIndex)
                                        itemFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 16, -yOffset)
                                        itemFrame:SetSize(scrollChild:GetWidth() - 25, ITEM_HEIGHT)
                                        self:UpdateItemFrame(itemFrame, itemEntry, boss.name)
                                        itemFrame:Show()
                                        yOffset = yOffset + ITEM_HEIGHT + 1
                                    end
                                end
                            end
                        end
                        
                        -- Render non-boss items
                        for _, itemEntry in ipairs(location.items) do
                            if self.showCollected or not itemEntry.isCollected then
                                itemIndex = itemIndex + 1
                                local itemFrame = self:GetOrCreateItemFrame(itemIndex)
                                itemFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 16, -yOffset)
                                itemFrame:SetSize(scrollChild:GetWidth() - 25, ITEM_HEIGHT)
                                self:UpdateItemFrame(itemFrame, itemEntry, nil)
                                itemFrame:Show()
                                yOffset = yOffset + ITEM_HEIGHT + 1
                            end
                        end
                        
                        yOffset = yOffset + 3
                    end
                end
                
                yOffset = yOffset + 8
            end
        end
    end
    
    scrollChild:SetHeight(math.max(yOffset + 20, 100))
end

-- =============================================================================
-- POPULATE FARMING ROUTE
-- =============================================================================

function Panel:PopulateFarmingRoute()
    local scrollChild = self.frame.scrollChild
    
    self:HideAllFrames()
    
    -- Collect all uncollected items with their locations
    local locationItems = {}  -- [locationName] = { items = {}, continent = "XX", order = N }
    
    for _, group in pairs(self.processedSources) do
        for _, itemEntry in ipairs(group.items) do
            if not itemEntry.isCollected or self.showCollected then
                local loc = itemEntry.sourceInfo.location or "Unknown"
                
                if not locationItems[loc] then
                    local routeInfo = ZONE_ROUTE_ORDER[loc] or { continent = "OTHER", order = 999 }
                    locationItems[loc] = {
                        name = loc,
                        continent = routeInfo.continent,
                        order = routeInfo.order,
                        items = {},
                        uncollectedCount = 0,
                    }
                end
                
                table.insert(locationItems[loc].items, itemEntry)
                if not itemEntry.isCollected then
                    locationItems[loc].uncollectedCount = locationItems[loc].uncollectedCount + 1
                end
            end
        end
    end
    
    -- Check if empty
    local totalLocations = 0
    for _ in pairs(locationItems) do totalLocations = totalLocations + 1 end
    
    if totalLocations == 0 then
        self.frame.emptyText:Show()
        scrollChild:SetHeight(100)
        return
    end
    
    self.frame.emptyText:Hide()
    
    -- Sort locations by continent, then by route order
    local sortedLocations = {}
    for _, locData in pairs(locationItems) do
        -- Only include if has uncollected items (when filtering)
        if self.showCollected or locData.uncollectedCount > 0 then
            table.insert(sortedLocations, locData)
        end
    end
    
    table.sort(sortedLocations, function(a, b)
        -- Sort by continent order first
        local contOrderA, contOrderB = 99, 99
        for i, cont in ipairs(CONTINENT_ORDER) do
            if a.continent == cont then contOrderA = i end
            if b.continent == cont then contOrderB = i end
        end
        
        if contOrderA ~= contOrderB then
            return contOrderA < contOrderB
        end
        
        -- Then by zone order within continent
        return (a.order or 999) < (b.order or 999)
    end)
    
    -- Render farming route
    local yOffset = 5
    local groupIndex = 0
    local itemIndex = 0
    local lastContinent = nil
    local stopNumber = 0
    
    for _, location in ipairs(sortedLocations) do
        -- Continent header
        if location.continent ~= lastContinent then
            lastContinent = location.continent
            groupIndex = groupIndex + 1
            
            local groupFrame = self:GetOrCreateGroupFrame(groupIndex)
            groupFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -yOffset)
            groupFrame:SetSize(scrollChild:GetWidth() - 5, GROUP_HEADER_HEIGHT)
            
            local contName = CONTINENT_NAMES[location.continent] or "Other Locations"
            groupFrame.text:SetText(string.format("|cffffd100%s|r", contName))
            groupFrame:Show()
            
            yOffset = yOffset + GROUP_HEADER_HEIGHT + 3
        end
        
        -- Location as a "stop" on the route
        stopNumber = stopNumber + 1
        local subgroupKey = "route_" .. stopNumber
        local locHeaderFrame = self:GetOrCreateSubgroupFrame(subgroupKey)
        locHeaderFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 8, -yOffset)
        locHeaderFrame:SetSize(scrollChild:GetWidth() - 15, SUBGROUP_HEADER_HEIGHT)
        
        local statusText = ""
        if location.uncollectedCount == 0 then
            statusText = " |cff00ff00[DONE]|r"
        else
            statusText = string.format(" |cffff8800[%d needed]|r", location.uncollectedCount)
        end
        
        locHeaderFrame.text:SetText(string.format("|cff00ffff#%d|r |cffffffff%s|r%s |cff666666(%d items)|r",
            stopNumber, location.name, statusText, #location.items))
        locHeaderFrame:Show()
        
        yOffset = yOffset + SUBGROUP_HEADER_HEIGHT + 2
        
        -- Items at this location
        for _, itemEntry in ipairs(location.items) do
            if self.showCollected or not itemEntry.isCollected then
                itemIndex = itemIndex + 1
                local itemFrame = self:GetOrCreateItemFrame(itemIndex)
                itemFrame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 16, -yOffset)
                itemFrame:SetSize(scrollChild:GetWidth() - 25, ITEM_HEIGHT)
                
                -- Show source type instead of boss for route view
                local sourceLabel = itemEntry.sourceInfo.bossName or itemEntry.sourceInfo.sourceName or ""
                self:UpdateItemFrame(itemFrame, itemEntry, sourceLabel)
                itemFrame:Show()
                yOffset = yOffset + ITEM_HEIGHT + 1
            end
        end
        
        yOffset = yOffset + 5
    end
    
    scrollChild:SetHeight(math.max(yOffset + 20, 100))
end

-- =============================================================================
-- FRAME POOL MANAGEMENT
-- =============================================================================

function Panel:HideAllFrames()
    for _, frame in pairs(self.groupFrames) do frame:Hide() end
    for _, frame in pairs(self.itemFrames) do frame:Hide() end
    for _, frame in pairs(self.subgroupFrames) do frame:Hide() end
end

function Panel:GetOrCreateGroupFrame(index)
    if not self.groupFrames[index] then
        local f = CreateFrame("Frame", nil, self.frame.scrollChild)
        f:SetHeight(GROUP_HEADER_HEIGHT)
        
        f.bg = f:CreateTexture(nil, "BACKGROUND")
        f.bg:SetAllPoints()
        f.bg:SetColorTexture(0.15, 0.12, 0.05, 0.9)
        
        f.text = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        f.text:SetPoint("LEFT", 5, 0)
        f.text:SetJustifyH("LEFT")
        
        self.groupFrames[index] = f
    end
    return self.groupFrames[index]
end

function Panel:GetOrCreateSubgroupFrame(key)
    if not self.subgroupFrames[key] then
        local f = CreateFrame("Frame", nil, self.frame.scrollChild)
        f:SetHeight(SUBGROUP_HEADER_HEIGHT)
        
        f.bg = f:CreateTexture(nil, "BACKGROUND")
        f.bg:SetAllPoints()
        f.bg:SetColorTexture(0.08, 0.12, 0.15, 0.8)
        
        f.text = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        f.text:SetPoint("LEFT", 5, 0)
        f.text:SetJustifyH("LEFT")
        
        self.subgroupFrames[key] = f
    end
    return self.subgroupFrames[key]
end

function Panel:GetOrCreateItemFrame(index)
    if not self.itemFrames[index] then
        local f = CreateFrame("Button", nil, self.frame.scrollChild)
        f:SetHeight(ITEM_HEIGHT)
        
        f.bg = f:CreateTexture(nil, "BACKGROUND")
        f.bg:SetAllPoints()
        
        f.icon = f:CreateTexture(nil, "ARTWORK")
        f.icon:SetSize(28, 28)
        f.icon:SetPoint("LEFT", 4, 0)
        
        f.iconBorder = f:CreateTexture(nil, "OVERLAY")
        f.iconBorder:SetSize(30, 30)
        f.iconBorder:SetPoint("CENTER", f.icon, "CENTER", 0, 0)
        f.iconBorder:SetTexture("Interface\\Common\\WhiteIconFrame")
        
        f.statusIcon = f:CreateTexture(nil, "OVERLAY")
        f.statusIcon:SetSize(14, 14)
        f.statusIcon:SetPoint("BOTTOMRIGHT", f.icon, "BOTTOMRIGHT", 3, -3)
        
        f.itemName = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        f.itemName:SetPoint("TOPLEFT", f.icon, "TOPRIGHT", 6, -2)
        f.itemName:SetWidth(250)
        f.itemName:SetJustifyH("LEFT")
        
        f.sourceText = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        f.sourceText:SetPoint("BOTTOMLEFT", f.icon, "BOTTOMRIGHT", 6, 2)
        f.sourceText:SetWidth(250)
        f.sourceText:SetJustifyH("LEFT")
        f.sourceText:SetTextColor(0.6, 0.6, 0.6)
        
        f.slotText = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        f.slotText:SetPoint("RIGHT", -4, 0)
        f.slotText:SetTextColor(0.5, 0.5, 0.5)
        
        f:SetScript("OnEnter", function(self)
            if self.itemEntry and self.itemEntry.item then
                GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                if self.itemEntry.item.itemID then
                    ETB:SafeSetTooltipItem(GameTooltip, self.itemEntry.item.itemID)
                end
                GameTooltip:Show()
                
                if self.itemEntry.isCollected then
                    self.bg:SetColorTexture(0.1, 0.25, 0.1, 0.9)
                else
                    self.bg:SetColorTexture(0.25, 0.18, 0.1, 0.9)
                end
            end
        end)
        f:SetScript("OnLeave", function(self)
            GameTooltip:Hide()
            if self.itemEntry then
                if self.itemEntry.isCollected then
                    self.bg:SetColorTexture(0.08, 0.18, 0.08, 0.7)
                else
                    self.bg:SetColorTexture(0.18, 0.12, 0.08, 0.7)
                end
            end
        end)
        
        f:SetScript("OnClick", function(self)
            if self.itemEntry and self.itemEntry.item and self.itemEntry.item.itemID and IsShiftKeyDown() then
                local _, link = GetItemInfo(self.itemEntry.item.itemID)
                if link then ChatEdit_InsertLink(link) end
            end
        end)
        
        self.itemFrames[index] = f
    end
    return self.itemFrames[index]
end

function Panel:UpdateItemFrame(frame, itemEntry, sourceLabel)
    local item = itemEntry.item
    frame.itemEntry = itemEntry
    frame._sourceLabel = sourceLabel  -- v15.8.3: stored for async re-render

    -- v15.8.3: Resolve icon + name at render time. item.icon / item.name
    -- can be nil if the upstream caller (the main browser's live preview
    -- scanner) captured them before GetItemInfo finished its async load.
    -- GetItemIcon is synchronous in MoP 5.4.8 and returns the icon path
    -- immediately (iconID is static client data). GetItemInfo may still
    -- be async; fall back to the upstream-cached value or "Item <id>".
    local resolvedIcon = item.icon
    local resolvedName = item.name
    local resolvedQuality = item.quality
    if item.itemID then
        if not resolvedIcon and GetItemIcon then
            resolvedIcon = GetItemIcon(item.itemID)
        end
        if not resolvedName then
            local n, _, q = GetItemInfo(item.itemID)
            resolvedName = n
            resolvedQuality = resolvedQuality or q
            if not n then
                -- Trigger async load + arm panel refresh on completion
                if not self._pendingItemNames then self._pendingItemNames = {} end
                self._pendingItemNames[item.itemID] = true
                self:_armNameRefreshListener()
            end
        end
    end

    frame.icon:SetTexture(resolvedIcon or "Interface\\Icons\\INV_Misc_QuestionMark")

    local quality = resolvedQuality or 2
    local r, g, b = GetItemQualityColor(quality)
    frame.iconBorder:SetVertexColor(r, g, b)

    local qualityColor = ETB:GetQualityColor(quality)
    frame.itemName:SetText(qualityColor .. (resolvedName or ("Item " .. tostring(item.itemID))) .. "|r")

    frame.sourceText:SetText(sourceLabel or "")
    frame.slotText:SetText("|cff666666[" .. itemEntry.slotName .. "]|r")

    if itemEntry.isCollected then
        frame.bg:SetColorTexture(0.08, 0.18, 0.08, 0.7)
        frame.statusIcon:SetTexture("Interface\\RaidFrame\\ReadyCheck-Ready")
        frame.statusIcon:Show()
        frame.itemName:SetAlpha(0.7)
    else
        frame.bg:SetColorTexture(0.18, 0.12, 0.08, 0.7)
        frame.statusIcon:SetTexture("Interface\\RaidFrame\\ReadyCheck-NotReady")
        frame.statusIcon:Show()
        frame.itemName:SetAlpha(1.0)
    end
end

-- v15.8.3: One-shot listener for items whose name was still async-loading
-- when first rendered. When GET_ITEM_INFO_RECEIVED fires for one of them,
-- repopulate the visible item frames. Self-installing; called from
-- UpdateItemFrame whenever it sees a pending itemID.
function Panel:_armNameRefreshListener()
    if not self._pendingItemNames then return end
    if not self._nameListener then
        self._nameListener = CreateFrame("Frame")
        self._nameListener:SetScript("OnEvent", function(_, _, evtItemID)
            if not self._pendingItemNames or not self._pendingItemNames[evtItemID] then return end
            self._pendingItemNames[evtItemID] = nil
            -- Repopulate visible item frames so the resolved name + icon show
            if self.frame and self.frame:IsShown() and self.RefreshVisibleItems then
                self:RefreshVisibleItems()
            end
        end)
        self._nameListener:RegisterEvent("GET_ITEM_INFO_RECEIVED")
    end
end

-- Refresh visible item frames in-place (called by the async listener).
-- Walks the same processedSources data; each itemFrame's UpdateItemFrame
-- re-resolves icon/name from the API and updates the texture/text.
function Panel:RefreshVisibleItems()
    if not self.processedSources or not self.itemFrames then return end
    -- The frame->itemEntry binding is stable; UpdateItemFrame just needs to
    -- be re-invoked on each visible frame with its stored itemEntry.
    for _, f in ipairs(self.itemFrames) do
        if f.itemEntry and f:IsShown() then
            self:UpdateItemFrame(f, f.itemEntry, f._sourceLabel or "")
        end
    end
end

-- =============================================================================
-- UPDATE STATS & FILTERS
-- =============================================================================

function Panel:UpdateStats()
    local totalItems = 0
    local collectedItems = 0
    local locationCounts = { raids = 0, dungeons = 0, zones = 0 }
    
    for _, group in pairs(self.processedSources) do
        totalItems = totalItems + group.totalItems
        collectedItems = collectedItems + group.collectedItems
        
        for _, locData in pairs(group.locations) do
            if locData.totalItems > locData.collectedItems then
                if group.type == "RAID" then locationCounts.raids = locationCounts.raids + 1
                elseif group.type == "DUNGEON" then locationCounts.dungeons = locationCounts.dungeons + 1
                else locationCounts.zones = locationCounts.zones + 1
                end
            end
        end
    end
    
    local percent = totalItems > 0 and math.floor((collectedItems / totalItems) * 100) or 0
    local progressColor = percent == 100 and "|cff00ff00" or (percent >= 50 and "|cffffd100" or "|cffff8800")
    self.frame.progressText:SetText(string.format("%s%d/%d|r items collected |cff888888(%d%%)|r",
        progressColor, collectedItems, totalItems, percent))
    
    local locParts = {}
    if locationCounts.raids > 0 then table.insert(locParts, locationCounts.raids .. " raid" .. (locationCounts.raids > 1 and "s" or "")) end
    if locationCounts.dungeons > 0 then table.insert(locParts, locationCounts.dungeons .. " dungeon" .. (locationCounts.dungeons > 1 and "s" or "")) end
    if locationCounts.zones > 0 then table.insert(locParts, locationCounts.zones .. " zone" .. (locationCounts.zones > 1 and "s" or "")) end
    
    if #locParts > 0 then
        self.frame.locationText:SetText("Locations: " .. table.concat(locParts, ", "))
    else
        self.frame.locationText:SetText("|cff00ff00All items collected!|r")
    end
    
    local uncollected = totalItems - collectedItems
    if uncollected > 0 then
        self.frame.uncollectedText:SetText("|cffff8800" .. uncollected .. " needed|r")
    else
        self.frame.uncollectedText:SetText("|cff00ff00Complete!|r")
    end
end

function Panel:UpdateFilterButtons()
    if self.showCollected then
        self.frame.showAllBtn:GetFontString():SetTextColor(0.2, 1, 0.2)
        self.frame.uncollectedOnlyBtn:GetFontString():SetTextColor(1, 0.82, 0)
    else
        self.frame.showAllBtn:GetFontString():SetTextColor(1, 0.82, 0)
        self.frame.uncollectedOnlyBtn:GetFontString():SetTextColor(0.2, 1, 0.2)
    end
end

ETB:Debug("SourcesSummaryPanel", "SourcesSummaryPanel.lua loaded")
