--[[
============================================================================
ETB v11.0 - SetsUI.lua
============================================================================
UI for browsing Blizzard transmog sets - redesigned to match Blizzard style.
Shows full mannequin with item icons for each slot.
============================================================================
]]--

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

ETB.SetsUI = {
    frame = nil,
    setListFrame = nil,
    setDetailFrame = nil,
    setButtons = {},
    selectedSetID = nil,
    currentFilter = "blizzard",  -- "blizzard", "custom", "collected", "incomplete", "all", "community"
    searchText = "",
    armorFilter = "all",       -- "all", "CLOTH", "LEATHER", "MAIL", "PLATE"
    expansionFilter = "all",   -- "all", 0=Classic, 1=TBC, 2=Wrath, 3=Cata, 4=MoP
    classFilter = "all",       -- "all", or classMask number (1=Warrior, 2=Paladin, etc.)
    colorFilter = nil,         -- nil = all, or "RED", "BLUE", etc.
    sortedSets = {},
    pieceButtons = {},  -- Buttons for each set piece
}

local SetsUI = ETB.SetsUI

-- Slot order for displaying set pieces (matches Blizzard's order)
local SLOT_ORDER = {
    "HEADSLOT", "SHOULDERSLOT", "CHESTSLOT", "WAISTSLOT",
    "LEGSSLOT", "FEETSLOT", "WRISTSLOT", "HANDSSLOT"
}

local SLOT_NAMES = {
    HEADSLOT = "Head",
    SHOULDERSLOT = "Shoulders", 
    CHESTSLOT = "Chest",
    WAISTSLOT = "Waist",
    LEGSSLOT = "Legs",
    FEETSLOT = "Feet",
    WRISTSLOT = "Wrists",
    HANDSSLOT = "Hands",
}

-- Map INVTYPE strings → button index (for Blizzard sets)
local INVTYPE_TO_BUTTON = {
    INVTYPE_HEAD = 1,
    INVTYPE_SHOULDER = 2,
    INVTYPE_CHEST = 3,
    INVTYPE_ROBE = 3,       -- Robe = Chest slot
    INVTYPE_WAIST = 4,
    INVTYPE_LEGS = 5,
    INVTYPE_FEET = 6,
    INVTYPE_WRIST = 7,
    INVTYPE_HAND = 8,
}

-- Map invSlot numbers → button index (for Community/Custom sets)
-- Unified with Blizzard order: Head/Shoulder/Chest/Waist/Legs/Feet/Wrist/Hands
local INVSLOT_TO_BUTTON = {
    [1] = 1,   -- Head
    [3] = 2,   -- Shoulder
    [5] = 3,   -- Chest
    [20] = 3,  -- Robe = Chest
    [6] = 4,   -- Waist
    [7] = 5,   -- Legs
    [8] = 6,   -- Feet
    [9] = 7,   -- Wrist
    [10] = 8,  -- Hands
}

-- Button index → invSlot number (for Community/Custom sets)
local BUTTON_TO_INVSLOT = { 1, 3, 5, 6, 7, 8, 9, 10 }

-- Shared slot names by invSlot number
local INVSLOT_NAMES = {
    [1] = "Head", [3] = "Shoulders", [5] = "Chest", [6] = "Waist",
    [7] = "Legs", [8] = "Feet", [9] = "Wrists", [10] = "Hands",
    [20] = "Chest",
}

-- Class mask to class name mapping for tier sets
-- Blizzard uses a bitmask where each bit represents a class
local CLASS_MASK_MAP = {
    [1] = "Warrior",
    [2] = "Paladin",
    [4] = "Hunter",
    [8] = "Rogue",
    [16] = "Priest",
    [32] = "Death Knight",
    [64] = "Shaman",
    [128] = "Mage",
    [256] = "Warlock",
    [512] = "Monk",
    [1024] = "Druid",
}

-- Class colors for display
local CLASS_COLORS = {
    Warrior = "|cffC79C6E",
    Paladin = "|cffF58CBA",
    Hunter = "|cffABD473",
    Rogue = "|cffFFF569",
    Priest = "|cffFFFFFF",
    ["Death Knight"] = "|cffC41F3B",
    Shaman = "|cff0070DE",
    Mage = "|cff69CCF0",
    Warlock = "|cff9482C9",
    Monk = "|cff00FF96",
    Druid = "|cffFF7D0A",
}

-- Helper to get class name from classMask
local function GetClassFromMask(classMask)
    if not classMask or classMask == 0 then return nil end
    -- Check for single class mask
    for mask, className in pairs(CLASS_MASK_MAP) do
        if classMask == mask then
            return className
        end
    end
    -- If multiple classes (like all plate classes), return nil
    return nil
end

-- Faction icons/colors
local FACTION_INFO = {
    Alliance = { icon = "|TInterface\\PVPFrame\\PVP-Currency-Alliance:14:14|t", color = "|cff0078ff" },
    Horde = { icon = "|TInterface\\PVPFrame\\PVP-Currency-Horde:14:14|t", color = "|cffff2020" },
}

-- Helper to get faction display string
local function GetFactionDisplay(faction)
    if not faction then return nil end
    local info = FACTION_INFO[faction]
    if info then
        return info.icon .. " " .. info.color .. faction .. "|r"
    end
    return nil
end

-- =============================================================================
-- CREATE SETS PANEL (Split View: List on Left, Detail on Right)
-- =============================================================================

function SetsUI:Create(parent)
    if self.frame then return self.frame end
    
    local f = CreateFrame("Frame", "ETBSetsUI", parent, "BackdropTemplate")
    f:SetAllPoints(parent)
    f:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    
    -- Create left panel (set list)
    self:CreateSetListPanel(f)
    
    -- Create right panel (set detail like Blizzard's UI)
    self:CreateSetDetailPanel(f)
    
    self.frame = f
    return self.frame
end

function SetsUI:CreateSetListPanel(parent)
    local panel = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    panel:SetSize(280, parent:GetHeight() - 20)
    panel:SetPoint("TOPLEFT", 10, -10)
    panel:SetPoint("BOTTOMLEFT", 10, 10)
    panel:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 10,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    panel:SetClipsChildren(true)
    self.setListFrame = panel
    
    -- Header
    local header = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    header:SetPoint("TOP", 0, -8)
    header:SetText("|cff00ffffTransmog Sets|r")
    
    -- Row 1: Source type filters
    local filterY1 = -30
    panel.filterBlizzard = self:CreateSmallFilterButton(panel, "Blizzard", 8, filterY1, "blizzard", 48)
    panel.filterCustom = self:CreateSmallFilterButton(panel, "Custom", 59, filterY1, "custom", 44)
    panel.filterCommunity = self:CreateSmallFilterButton(panel, "Community", 106, filterY1, "community", 62)
    panel.filterAll = self:CreateSmallFilterButton(panel, "All", 171, filterY1, "all", 30)
    
    -- Row 2: Completion + Builder
    local filterY2 = -50
    panel.filterIncomplete = self:CreateSmallFilterButton(panel, "Missing", 8, filterY2, "incomplete", 50)
    panel.filterCollected = self:CreateSmallFilterButton(panel, "Done", 61, filterY2, "collected", 40)
    
    panel.setBuilderBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    panel.setBuilderBtn:SetSize(65, 18)
    panel.setBuilderBtn:SetPoint("TOPLEFT", 105, filterY2)
    panel.setBuilderBtn:SetText("|cffffd100Builder|r")
    panel.setBuilderBtn:SetScript("OnClick", function()
        if ETB.SetBuilder then
            if _G.ETBMainFrame and _G.ETBMainFrame:IsShown() then
                ETB._builderHidMainFrame = true
                _G.ETBMainFrame:Hide()
            end
            ETB.SetBuilder:Show()
        end
    end)
    panel.setBuilderBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText("Set Builder")
        GameTooltip:AddLine("Open Set Builder in a separate window", 0.8, 0.8, 0.8)
        GameTooltip:Show()
    end)
    panel.setBuilderBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    
    -- Row 3: Armor Type dropdown
    local dropW = 128
    local ddY = -72
    
    local armorLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    armorLabel:SetPoint("TOPLEFT", 8, ddY)
    armorLabel:SetText("|cffaaaaaaArmor:|r")
    
    panel.armorDropdown = ETBDropdown:Create("ETBSetsArmorDD", panel)
    panel.armorDropdown:SetPoint("TOPLEFT", 48, ddY + 3)
    ETBDropdown:SetWidth(panel.armorDropdown, dropW)
    ETBDropdown:SetText(panel.armorDropdown, "All Types")
    ETBDropdown:Initialize(panel.armorDropdown, function(dd, _items_)
        local info = ETBDropdown:CreateInfo()
        local types = {
            { text = "All Types", value = "all" },
            { text = "Cloth", value = "CLOTH" },
            { text = "Leather", value = "LEATHER" },
            { text = "Mail", value = "MAIL" },
            { text = "Plate", value = "PLATE" },
        }
        for _, t in ipairs(types) do
            info.text = t.text
            info.value = t.value
            info.func = function()
                ETBDropdown:SetText(panel.armorDropdown, t.text)
                SetsUI.armorFilter = t.value
                SetsUI:RefreshSetList()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    
    -- Row 4: Expansion dropdown
    local expY = ddY - 22
    local expLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    expLabel:SetPoint("TOPLEFT", 8, expY)
    expLabel:SetText("|cffaaaaaaExp:|r")
    
    panel.expansionDropdown = ETBDropdown:Create("ETBSetsExpDD", panel)
    panel.expansionDropdown:SetPoint("TOPLEFT", 48, expY + 3)
    ETBDropdown:SetWidth(panel.expansionDropdown, dropW)
    ETBDropdown:SetText(panel.expansionDropdown, "All Expansions")
    ETBDropdown:Initialize(panel.expansionDropdown, function(dd, _items_)
        local info = ETBDropdown:CreateInfo()
        local expansions = {
            { text = "All Expansions", value = "all" },
            { text = "Classic", value = 0 },
            { text = "Burning Crusade", value = 1 },
            { text = "Wrath", value = 2 },
            { text = "Cataclysm", value = 3 },
            { text = "Mists of Pandaria", value = 4 },
        }
        for _, e in ipairs(expansions) do
            info.text = e.text
            info.value = e.value
            info.func = function()
                ETBDropdown:SetText(panel.expansionDropdown, e.text)
                SetsUI.expansionFilter = e.value
                SetsUI:RefreshSetList()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    
    -- Row 5: Class dropdown
    local classY = expY - 22
    local classLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    classLabel:SetPoint("TOPLEFT", 8, classY)
    classLabel:SetText("|cffaaaaaaClass:|r")
    
    panel.classDropdown = ETBDropdown:Create("ETBSetsClassDD", panel)
    panel.classDropdown:SetPoint("TOPLEFT", 48, classY + 3)
    ETBDropdown:SetWidth(panel.classDropdown, dropW)
    ETBDropdown:SetText(panel.classDropdown, "All Classes")
    ETBDropdown:Initialize(panel.classDropdown, function(dd, _items_)
        local info = ETBDropdown:CreateInfo()
        local classes = {
            { text = "All Classes", value = "all" },
            { text = "Warrior", value = 1 },
            { text = "Paladin", value = 2 },
            { text = "Hunter", value = 4 },
            { text = "Rogue", value = 8 },
            { text = "Priest", value = 16 },
            { text = "Death Knight", value = 32 },
            { text = "Shaman", value = 64 },
            { text = "Mage", value = 128 },
            { text = "Warlock", value = 256 },
            { text = "Monk", value = 512 },
            { text = "Druid", value = 1024 },
        }
        for _, c in ipairs(classes) do
            info.text = c.text
            info.value = c.value
            info.func = function()
                ETBDropdown:SetText(panel.classDropdown, c.text)
                SetsUI.classFilter = c.value
                SetsUI:RefreshSetList()
            end
            ETBDropdown:AddButton(_items_, info)
        end
    end)
    
    -- Color filter dropdown
    local colorY = classY - 22
    local colorLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    colorLabel:SetPoint("TOPLEFT", 8, colorY)
    colorLabel:SetText("|cffaaaaaaColor:|r")
    
    panel.colorDropdown = ETBDropdown:Create("ETBSetsColorDD", panel)
    panel.colorDropdown:SetPoint("TOPLEFT", 48, colorY + 3)
    ETBDropdown:SetWidth(panel.colorDropdown, dropW)
    ETBDropdown:SetText(panel.colorDropdown, "All Colors")
    ETBDropdown:Initialize(panel.colorDropdown, function(dd, _items_)
        local info = ETBDropdown:CreateInfo()
        info.text = "All Colors"
        info.value = nil
        info.func = function()
            ETBDropdown:SetText(panel.colorDropdown, "All Colors")
            SetsUI.colorFilter = nil
            SetsUI:RefreshSetList()
        end
        ETBDropdown:AddButton(_items_, info)
        
        if ETB.ColorSearch then
            for _, cat in ipairs(ETB.ColorSearch:GetCategories()) do
                info.text = cat.name
                info.value = cat.id
                info.func = function()
                    ETBDropdown:SetText(panel.colorDropdown, cat.name)
                    SetsUI.colorFilter = cat.id
                    SetsUI:RefreshSetList()
                end
                ETBDropdown:AddButton(_items_, info)
            end
        end
    end)
    
    -- Search box
    panel.searchBox = CreateFrame("EditBox", nil, panel, "InputBoxTemplate")
    panel.searchBox:SetSize(260, 18)
    panel.searchBox:SetPoint("TOP", 0, colorY - 20)
    panel.searchBox:SetAutoFocus(false)
    panel.searchBox:SetScript("OnTextChanged", function(eb)
        SetsUI.searchText = eb:GetText():lower()
        SetsUI:RefreshSetList()
    end)
    panel.searchBox:SetScript("OnEscapePressed", function(eb) eb:ClearFocus() end)
    
    -- Stats
    panel.stats = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    panel.stats:SetPoint("TOP", panel.searchBox, "BOTTOM", 0, -3)
    panel.stats:SetText("|cff888888Loading sets...|r")
    
    -- Scroll frame
    local scrollContainer = CreateFrame("Frame", nil, panel)
    scrollContainer:SetPoint("TOPLEFT", 5, colorY - 55)
    scrollContainer:SetPoint("BOTTOMRIGHT", -5, 5)
    
    local scrollFrame = CreateFrame("ScrollFrame", nil, scrollContainer, "UIPanelScrollFrameTemplate")
    scrollFrame:SetAllPoints()
    
    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetSize(scrollFrame:GetWidth() - 20, 1)
    scrollFrame:SetScrollChild(scrollChild)
    
    panel.scrollFrame = scrollFrame
    panel.scrollChild = scrollChild
    
    parent.setListPanel = panel
    
    -- =========================================================================
    -- COLLECTION UPDATE EVENT — refresh sets when appearances change
    -- =========================================================================
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("TRANSMOG_COLLECTION_UPDATED")
    eventFrame:SetScript("OnEvent", function(ef, event)
        if SetsUI._lastCollectionUpdate and (GetTime() - SetsUI._lastCollectionUpdate) < 1.0 then
            return
        end
        SetsUI._lastCollectionUpdate = GetTime()
        
        -- Invalidate caches that depend on collection state
        SetsUI._completionCache = nil
        SetsUI._setColorCache = nil
        
        if ETB.CommunitySets and ETB.CommunitySets.UpdateCollectionCounts then
            ETB.CommunitySets:UpdateCollectionCounts()
        end
        if ETB.SetsData then
            ETB.SetsData.setCompletion = {}
        end
        
        if SetsUI.frame and SetsUI.frame:IsShown() then
            SetsUI:RefreshSetList()
            if SetsUI.selectedSetID then
                local sid = SetsUI.selectedSetID
                if type(sid) == "string" and sid:find("COMMUNITY_") then
                    local cs = ETB.CommunitySets and ETB.CommunitySets:GetSet(sid)
                    if cs then SetsUI:UpdateCommunitySetPieceIcons(sid, cs) end
                elseif type(sid) == "number" then
                    SetsUI:UpdatePieceIcons(sid)
                end
            end
        end
    end)
end

-- =============================================================================
-- RIGHT PANEL: SET DETAIL (Blizzard Style)
-- =============================================================================

function SetsUI:CreateSetDetailPanel(parent)
    local panel = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    panel:SetPoint("TOPLEFT", self.setListFrame, "TOPRIGHT", 10, 0)
    panel:SetPoint("BOTTOMRIGHT", -10, 10)
    panel:SetBackdrop({
        bgFile = "Interface\\Collections\\CollectionsBackgroundTile",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
        tile = true, tileSize = 256, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 }
    })
    self.setDetailFrame = panel
    
    -- Variant dropdown (top right) - for Normal/Heroic/etc
    panel.variantDropdown = ETBDropdown:Create("ETBSetsVariantDropdown", panel)
    panel.variantDropdown:SetPoint("TOPRIGHT", -5, -5)
    ETBDropdown:SetWidth(panel.variantDropdown, 120)
    ETBDropdown:SetText(panel.variantDropdown, "Select Variant")
    
    -- Set name (large gold text)
    panel.setName = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    panel.setName:SetPoint("TOP", 0, -40)
    panel.setName:SetTextColor(1, 0.82, 0)
    panel.setName:SetText("Select a Set")
    
    -- Source location (smaller text below name)
    panel.sourceName = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    panel.sourceName:SetPoint("TOP", panel.setName, "BOTTOM", 0, -5)
    panel.sourceName:SetTextColor(0.8, 0.8, 0.8)
    panel.sourceName:SetText("")
    
    -- Item icons row (show each set piece)
    self:CreatePieceIcons(panel)
    
    -- Full mannequin model (large, centered)
    panel.model = CreateFrame("DressUpModel", nil, panel)
    panel.model:SetSize(350, 480)  -- Larger preview
    panel.model:SetPoint("CENTER", 0, -30)
    panel.model:SetUnit("player")
    panel.model:SetRotation(0)
    panel.model:EnableMouse(true)
    panel.model:EnableMouseWheel(true)
    
    -- Model controls
    panel.model:SetScript("OnMouseDown", function(self, btn)
        if btn == "LeftButton" then
            self.rotating = true
            self.startX = GetCursorPosition()
        end
    end)
    panel.model:SetScript("OnMouseUp", function(self) self.rotating = false end)
    panel.model:SetScript("OnUpdate", function(self)
        if self.rotating then
            local x = GetCursorPosition()
            self.rotation = (self.rotation or 0) + (x - self.startX) * 0.01
            self:SetRotation(self.rotation)
            self.startX = x
        end
    end)
    panel.model:SetScript("OnMouseWheel", function(self, delta)
        local x, y, z = self:GetPosition()
        self:SetPosition(x, y, math.max(-1, math.min(3, z + delta * 0.15)))
    end)
    
    -- Completion text at bottom
    panel.completion = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    panel.completion:SetPoint("BOTTOM", 0, 100)
    panel.completion:SetText("")
    
    -- Hint text
    panel.hint = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    panel.hint:SetPoint("BOTTOM", 0, 80)
    panel.hint:SetText("|cff888888Drag to rotate • Scroll to zoom|r")
    
    -- Show Sources button (for alternatives)
    panel.showSourcesBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    panel.showSourcesBtn:SetSize(140, 24)
    panel.showSourcesBtn:SetPoint("BOTTOMLEFT", 60, 15)
    panel.showSourcesBtn:SetText("Show Sources")
    panel.showSourcesBtn:SetScript("OnClick", function()
        SetsUI:ShowSetSources()
    end)
    
    -- Delete Custom Set button (only visible for user-defined sets)
    panel.deleteSetBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    panel.deleteSetBtn:SetSize(140, 24)
    panel.deleteSetBtn:SetPoint("BOTTOMRIGHT", -60, 15)
    panel.deleteSetBtn:SetText("Delete Set")
    panel.deleteSetBtn:Hide()  -- Hidden by default
    panel.deleteSetBtn:SetScript("OnClick", function()
        SetsUI:ConfirmDeleteSet()
    end)
    
    parent.setDetailPanel = panel
end

-- =============================================================================
-- PIECE ICONS (Like Blizzard's row of slot icons)
-- =============================================================================

function SetsUI:CreatePieceIcons(parent)
    local iconSize = 55  -- Larger icons
    local spacing = 6
    local totalWidth = (#SLOT_ORDER * iconSize) + ((#SLOT_ORDER - 1) * spacing)
    local startX = -totalWidth / 2
    
    self.pieceButtons = {}
    
    for i, slotName in ipairs(SLOT_ORDER) do
        local btn = CreateFrame("Button", nil, parent)
        btn:SetSize(iconSize, iconSize + 16)  -- Extra height for name label
        btn:SetPoint("TOP", startX + (i-1) * (iconSize + spacing) + (iconSize/2), -85)
        
        -- Background
        btn.bg = btn:CreateTexture(nil, "BACKGROUND")
        btn.bg:SetSize(iconSize, iconSize)
        btn.bg:SetPoint("TOP")
        btn.bg:SetTexture("Interface\\PaperDoll\\UI-Backpack-EmptySlot")
        
        -- Icon
        btn.icon = btn:CreateTexture(nil, "ARTWORK")
        btn.icon:SetSize(iconSize - 4, iconSize - 4)
        btn.icon:SetPoint("TOP", 0, -2)
        btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        
        -- Border (quality colored)
        btn.border = btn:CreateTexture(nil, "OVERLAY")
        btn.border:SetSize(iconSize + 2, iconSize + 2)
        btn.border:SetPoint("TOP", 0, 1)
        btn.border:SetTexture("Interface\\Common\\WhiteIconFrame")
        
        -- Collected checkmark
        btn.check = btn:CreateTexture(nil, "OVERLAY")
        btn.check:SetSize(22, 22)
        btn.check:SetPoint("BOTTOMRIGHT", btn.icon, "BOTTOMRIGHT", 4, -4)
        btn.check:SetTexture("Interface\\RaidFrame\\ReadyCheck-Ready")
        btn.check:Hide()
        
        -- Missing X
        btn.missing = btn:CreateTexture(nil, "OVERLAY")
        btn.missing:SetSize(22, 22)
        btn.missing:SetPoint("BOTTOMRIGHT", btn.icon, "BOTTOMRIGHT", 4, -4)
        btn.missing:SetTexture("Interface\\RaidFrame\\ReadyCheck-NotReady")
        btn.missing:Hide()
        
        -- Desaturate overlay for uncollected
        btn.desaturate = btn:CreateTexture(nil, "OVERLAY")
        btn.desaturate:SetAllPoints(btn.icon)
        btn.desaturate:SetColorTexture(0, 0, 0, 0.5)
        btn.desaturate:Hide()
        
        -- SLOT NAME label below icon (always shows slot name, not item name)
        btn.nameLabel = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        btn.nameLabel:SetPoint("TOP", btn.icon, "BOTTOM", 0, -2)
        btn.nameLabel:SetWidth(iconSize + 10)
        btn.nameLabel:SetHeight(14)
        btn.nameLabel:SetJustifyH("CENTER")
        btn.nameLabel:SetText(SLOT_NAMES[slotName] or "")
        btn.nameLabel:SetTextColor(0.7, 0.7, 0.7)
        
        -- Cycle button (for alternatives)
        btn.cycleBtn = CreateFrame("Button", nil, btn)
        btn.cycleBtn:SetSize(18, 18)
        btn.cycleBtn:SetPoint("TOPRIGHT", btn.icon, "TOPRIGHT", 4, 4)
        btn.cycleBtn:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
        btn.cycleBtn:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down")
        btn.cycleBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight", "ADD")
        btn.cycleBtn:SetScript("OnClick", function(self)
            SetsUI:CycleSlotAlternative(self:GetParent())
        end)
        btn.cycleBtn:SetScript("OnEnter", function(self)
            local parentBtn = self:GetParent()
            if parentBtn.allItems and #parentBtn.allItems > 1 then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:SetText("Cycle Alternative", 1, 1, 1)
                GameTooltip:AddLine(string.format("%d items can fill this slot", #parentBtn.allItems), 0.8, 0.8, 0.8)
                GameTooltip:AddLine(string.format("Currently showing: %d/%d", (parentBtn.currentIndex or 1), #parentBtn.allItems), 0.6, 0.6, 0.6)
                GameTooltip:AddLine(" ")
                for idx, item in ipairs(parentBtn.allItems) do
                    local marker = (idx == (parentBtn.currentIndex or 1)) and "|cff00ff00►|r " or "   "
                    local status = item.collected and "|cff00ff00✓|r" or "|cffff6600○|r"
                    local name = item.name or ("Item " .. (item.itemID or "?"))
                    GameTooltip:AddLine(marker .. status .. " " .. name, 1, 1, 1)
                end
                GameTooltip:Show()
            end
        end)
        btn.cycleBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        btn.cycleBtn:Hide()  -- Only show when there are alternatives
        
        btn.slotName = slotName
        btn.slotDisplayName = SLOT_NAMES[slotName]
        btn.slotIndex = i
        
        -- Alias for collected checkmark (for consistency)
        btn.collected = btn.check
        
        -- Tooltip with alternatives support
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            if self.itemID and self.itemID > 0 then
                ETB:SafeSetTooltipItem(GameTooltip, self.itemID)
                GameTooltip:AddLine(" ")
                
                -- Show slot status clearly
                if self.isCollected then
                    GameTooltip:AddLine("|cff00ff00✓ Slot Complete|r")
                else
                    GameTooltip:AddLine("|cffff4444✗ SLOT NEEDED|r")
                end
                
                -- Show item collection status
                if self.currentItemCollected then
                    GameTooltip:AddLine("|cff00ff00This item: Collected|r")
                else
                    GameTooltip:AddLine("|cffff6600This item: Not Collected|r")
                end
                
                -- Show alternatives info
                if self.allItems and #self.allItems > 1 then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine(string.format("|cffffd100%d items can fill this slot|r", #self.allItems))
                    GameTooltip:AddLine("|cff888888Click arrow to cycle|r")
                end
                
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("|cff888888Right-click to hide on preview|r")
            else
                GameTooltip:SetText(self.slotDisplayName or "Unknown Slot")
                GameTooltip:AddLine("|cff888888No item data|r", 1, 1, 1)
            end
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        
        -- Left-click to try on, Right-click to remove from preview
        btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        btn:SetScript("OnClick", function(self, button)
            local detail = SetsUI.setDetailFrame
            if not detail or not detail.model then return end
            
            if button == "LeftButton" then
                -- Left-click: Try on item
                if self.itemID and self.itemID > 0 then
                    detail.model:TryOn("item:" .. self.itemID)
                end
            elseif button == "RightButton" then
                -- Right-click: Remove this slot from preview
                if self.itemID and self.itemID > 0 then
                    -- Undress this specific slot using inventory slot ID
                    local invSlotID = SetsUI:GetInventorySlotID(self.slotName)
                    if invSlotID and detail.model.UndressSlot then
                        detail.model:UndressSlot(invSlotID)
                        -- Visual feedback - dim the icon
                        self.icon:SetDesaturated(true)
                        self.desaturate:Show()
                    end
                end
            end
        end)
        
        self.pieceButtons[i] = btn
    end
    
    -- Also store in parent (detail frame) for easy access
    parent.pieceButtons = self.pieceButtons
end

-- Helper to get inventory slot ID from our slot constant
function SetsUI:GetInventorySlotID(slotName)
    -- Map our SLOT_ORDER values to WoW inventory slot IDs
    local slotMapping = {
        [1] = 1,   -- Head -> INVSLOT_HEAD
        [2] = 3,   -- Shoulder -> INVSLOT_SHOULDER
        [3] = 5,   -- Chest -> INVSLOT_CHEST
        [4] = 6,   -- Waist -> INVSLOT_WAIST
        [5] = 7,   -- Legs -> INVSLOT_LEGS
        [6] = 8,   -- Feet -> INVSLOT_FEET
        [7] = 9,   -- Wrist -> INVSLOT_WRIST
        [8] = 10,  -- Hands -> INVSLOT_HAND
        [9] = 15,  -- Back -> INVSLOT_BACK
    }
    return slotMapping[slotName]
end

-- Cycle through alternative items for a slot
function SetsUI:CycleSlotAlternative(btn)
    if not btn.allItems or #btn.allItems <= 1 then return end
    
    local currentIndex = btn.currentIndex or 1
    local nextIndex = (currentIndex % #btn.allItems) + 1
    btn.currentIndex = nextIndex
    
    local item = btn.allItems[nextIndex]
    if item then
        -- Update the button display
        btn.itemID = item.itemID
        btn.currentItemCollected = item.collected
        
        -- Update icon
        if item.icon then
            btn.icon:SetTexture(item.icon)
        elseif item.itemID then
            local _, _, _, _, iconPath = GetItemInfoInstant(item.itemID)
            btn.icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark")
        end
        
        -- Update mannequin to show this item
        local detail = self.setDetailFrame
        if detail and detail.model then
            detail.model:TryOn("item:" .. item.itemID)
        end
        
        -- Update border based on THIS item's collection status
        if item.collected then
            btn.icon:SetDesaturated(false)
            btn.desaturate:Hide()
        else
            btn.icon:SetDesaturated(true)
            btn.desaturate:Show()
        end
    end
end

function SetsUI:UpdatePieceIcons(setID)
    -- COMPREHENSIVE RESET - Reset ALL icons to clean state
    for i, btn in ipairs(self.pieceButtons) do
        btn.icon:SetTexture("Interface\\PaperDoll\\UI-Backpack-EmptySlot")
        btn.icon:SetDesaturated(false)
        btn.border:SetVertexColor(0.3, 0.3, 0.3)  -- Gray for empty
        btn.check:Hide()
        btn.missing:Hide()
        btn.desaturate:Hide()
        btn.cycleBtn:Hide()
        btn.itemID = nil
        btn.isCollected = false
        btn.currentItemCollected = false
        btn.slotName = SLOT_ORDER[i]
        btn.alternatives = nil
        btn.allItems = nil
        btn.currentIndex = nil
        btn.primaryName = nil
        btn.primaryCollected = nil
        btn.invType = nil
        btn.slotData = nil
        if btn.altCount then btn.altCount:SetText("") end
        
        -- Reset name label to slot name
        if btn.nameLabel then
            btn.nameLabel:SetText(SLOT_NAMES[SLOT_ORDER[i]] or "")
            btn.nameLabel:SetTextColor(0.6, 0.6, 0.6)
        end
        
        -- Reset tooltips to empty slot
        local slotIndex = SLOT_ORDER[i]
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText(SLOT_NAMES[slotIndex] or "Empty Slot", 0.5, 0.5, 0.5)
            GameTooltip:AddLine("No item in this slot", 0.5, 0.5, 0.5)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    end
    
    if not setID then return end
    
    -- Get ALL items for this set (not grouped by slot - we'll do that ourselves)
    local allItems = ETB.SetsData:GetSetItems(setID)
    
    -- Group items by button position using INVTYPE mapping
    local itemsByButton = {}  -- [buttonIndex] = { primary=item, alternatives={}, collected=false }
    
    for _, item in ipairs(allItems) do
        local btnIdx = INVTYPE_TO_BUTTON[item.invType]
        if btnIdx then
            if not itemsByButton[btnIdx] then
                itemsByButton[btnIdx] = {
                    primary = item,
                    alternatives = {},
                    collected = item.collected or false,
                }
            else
                table.insert(itemsByButton[btnIdx].alternatives, item)
                if item.collected then
                    itemsByButton[btnIdx].collected = true
                end
            end
        end
    end
    
    -- Update each button that has items
    for btnIdx, slotData in pairs(itemsByButton) do
        local btn = self.pieceButtons[btnIdx]
        if btn and slotData and slotData.primary then
            local item = slotData.primary
            
            -- Build list of ALL items for this slot (primary + alternatives)
            local allSlotItems = {item}
            for _, alt in ipairs(slotData.alternatives) do
                table.insert(allSlotItems, alt)
            end
            
            btn.itemID = item.itemID
            btn.isCollected = slotData.collected  -- Use slot-level collection (any alternative counts)
            btn.currentItemCollected = item.collected  -- This specific item
            btn.invType = item.invType
            btn.alternatives = slotData.alternatives
            btn.allItems = allSlotItems  -- All items that can fill this slot
            btn.currentIndex = 1  -- Start with primary
            btn.primaryName = item.name
            btn.primaryCollected = item.collected
            
            -- Update item name label - SHOW SLOT NAME, not item name
            if btn.nameLabel then
                local slotDisplayName = SLOT_NAMES[SLOT_ORDER[btnIdx]] or "Slot"
                btn.nameLabel:SetText(slotDisplayName)
                if slotData.collected then
                    btn.nameLabel:SetTextColor(0.5, 1, 0.5)  -- Green for collected
                else
                    btn.nameLabel:SetTextColor(1, 0.5, 0.5)  -- Red for needed
                end
            end
            
            -- Set icon - try multiple methods
            local iconSet = false
            
            -- Method 1: Use item.icon if available
            if item.icon then
                btn.icon:SetTexture(item.icon)
                iconSet = true
            end
            
            -- Method 2: Try GetItemInfoInstant for immediate icon
            if not iconSet and item.itemID then
                local _, _, _, _, iconPath = GetItemInfoInstant(item.itemID)
                if iconPath then
                    btn.icon:SetTexture(iconPath)
                    iconSet = true
                end
            end
            
            -- Method 3: Try GetItemInfo (may be cached)
            if not iconSet and item.itemID then
                local _, _, _, _, _, _, _, _, _, itemIcon = GetItemInfo(item.itemID)
                if itemIcon then
                    btn.icon:SetTexture(itemIcon)
                    iconSet = true
                end
            end
            
            -- Update collected status (slot-level)
            if slotData.collected then
                btn.border:SetVertexColor(0, 1, 0)  -- Green border
                btn.check:Show()
                btn.desaturate:Hide()
            else
                btn.border:SetVertexColor(1, 0, 0)  -- Red border
                btn.missing:Show()
                btn.desaturate:Show()
            end
            
            -- Show cycle button if there are alternatives
            if #allSlotItems > 1 then
                btn.cycleBtn:Show()
            end
            if slotData.alternatives and #slotData.alternatives > 0 then
                if not btn.altCount then
                    btn.altCount = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                    btn.altCount:SetPoint("BOTTOMRIGHT", -2, 2)
                end
                btn.altCount:SetText("|cffffd100+" .. #slotData.alternatives .. "|r")
            end
            
            -- Set up proper tooltip for this item
            local capturedItemID = item.itemID
            local capturedSlotCollected = slotData.collected
            local capturedItemCollected = item.collected
            local capturedAltCount = #allSlotItems
            local capturedSlotName = SLOT_NAMES[SLOT_ORDER[btnIdx]] or "Unknown"
            
            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
                if capturedItemID and capturedItemID > 0 then
                    ETB:SafeSetTooltipItem(GameTooltip, capturedItemID)
                    GameTooltip:AddLine(" ")
                    
                    -- Show slot status clearly
                    GameTooltip:AddLine("|cffffd100" .. capturedSlotName .. "|r")
                    if capturedSlotCollected then
                        GameTooltip:AddLine("|cff00ff00✓ Slot Complete|r")
                    else
                        GameTooltip:AddLine("|cffff4444✗ SLOT NEEDED|r")
                    end
                    
                    -- Show item collection status
                    if capturedItemCollected then
                        GameTooltip:AddLine("|cff00ff00This item: Collected|r")
                    else
                        GameTooltip:AddLine("|cffff6600This item: Not Collected|r")
                    end
                    
                    -- Show alternatives info
                    if capturedAltCount > 1 then
                        GameTooltip:AddLine(" ")
                        GameTooltip:AddLine(string.format("|cffffd100%d items can fill this slot|r", capturedAltCount))
                        GameTooltip:AddLine("|cff888888Click arrow to cycle|r")
                    end
                else
                    GameTooltip:SetText(capturedSlotName)
                    GameTooltip:AddLine("|cff888888No item data|r", 1, 1, 1)
                end
                GameTooltip:Show()
            end)
            btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
    end
end

-- =============================================================================
-- HELPER FUNCTIONS
-- =============================================================================

function SetsUI:CreateSmallFilterButton(parent, text, x, y, filter, width)
    local btn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btn:SetSize(width or 45, 20)
    btn:SetPoint("TOPLEFT", x, y)
    btn:SetText(text)
    btn:GetFontString():SetFont(btn:GetFontString():GetFont(), 10)
    btn.filter = filter
    btn:SetScript("OnClick", function() SetsUI:SetFilter(filter) end)
    return btn
end

function SetsUI:UpdateFilterButtons()
    local panel = self.setListFrame
    if not panel then return end
    
    -- All filter buttons
    local buttons = {
        panel.filterBlizzard, 
        panel.filterCustom, 
        panel.filterCommunity,
         
        panel.filterIncomplete, 
        panel.filterCollected,
        panel.filterAll
    }
    
    for _, btn in ipairs(buttons) do
        if btn and btn.filter then
            local fs = btn:GetFontString()
            if fs then
                if btn.filter == self.currentFilter then
                    fs:SetTextColor(0.2, 1, 0.2)
                else
                    fs:SetTextColor(1, 0.82, 0)
                end
            end
        end
    end
end

-- =============================================================================
-- SET LIST
-- =============================================================================

-- Helper to check if player's class matches a classMask
local function PlayerClassMatchesMask(classMask)
    if not classMask or classMask == 0 then return true end  -- No restriction
    
    -- Get player's class ID (1-indexed bit position)
    local _, playerClass = UnitClass("player")
    local classIDBits = {
        WARRIOR = 1,
        PALADIN = 2,
        HUNTER = 4,
        ROGUE = 8,
        PRIEST = 16,
        DEATHKNIGHT = 32,
        SHAMAN = 64,
        MAGE = 128,
        WARLOCK = 256,
        MONK = 512,
        DRUID = 1024,
    }
    
    local playerBit = classIDBits[playerClass] or 0
    return bit.band(classMask, playerBit) ~= 0
end


-- Helper: Map classMask to primary armor type
local function ClassMaskToArmor(classMask)
    if not classMask or classMask == 0 then return nil end
    -- Cloth: Mage(128) + Warlock(256) + Priest(16)
    if bit.band(classMask, 128 + 256 + 16) > 0 then return "CLOTH" end
    -- Leather: Rogue(8) + Druid(1024) + Monk(512)
    if bit.band(classMask, 8 + 1024 + 512) > 0 then return "LEATHER" end
    -- Mail: Hunter(4) + Shaman(64)
    if bit.band(classMask, 4 + 64) > 0 then return "MAIL" end
    -- Plate: Warrior(1) + Paladin(2) + DeathKnight(32)
    if bit.band(classMask, 1 + 2 + 32) > 0 then return "PLATE" end
    return nil
end

-- Helper: Map expansion name string to ID number
local EXP_NAME_TO_ID = {
    ["Classic"] = 0, ["TBC"] = 1, ["Wrath"] = 2, ["Cata"] = 3, ["MoP"] = 4,
}

-- Helper: Check if a set passes the dropdown filters
local function PassesDropdownFilters(setData, armorFilter, expansionFilter, classFilter, colorFilter)
    -- Armor type filter
    if armorFilter ~= "all" then
        local setArmor = setData.armorType or ClassMaskToArmor(setData.classMask)
        if setArmor and setArmor ~= armorFilter then
            return false
        end
    end
    
    -- Expansion filter
    if expansionFilter ~= "all" then
        local setExp = nil
        -- Blizzard sets have numeric expansionID
        if setData.expansionID ~= nil then
            setExp = setData.expansionID
        -- Community/Custom sets have string expansion name
        elseif setData.expansion then
            setExp = EXP_NAME_TO_ID[setData.expansion]
        end
        if setExp ~= nil and setExp ~= expansionFilter then
            return false
        end
    end
    
    -- Class filter (classMask bitmask check)
    if classFilter ~= "all" then
        local cm = setData.classMask
        if cm and cm > 0 then
            if bit.band(cm, classFilter) == 0 then
                return false
            end
        end
        -- Community sets with specific armorType but no classMask:
        -- filter by armor type that matches the class
        if (not cm or cm == 0) and setData.armorType then
            local classArmor = ClassMaskToArmor(classFilter)
            if classArmor and setData.armorType ~= classArmor and setData.armorType ~= "COSMETIC" then
                return false
            end
        end
    end
    
    -- Color filter: set passes if ANY item has the selected color
    -- CRITICAL: NEVER call GetSetItems here — it creates 30+ tables per set.
    -- Use cached visualIDs from GetSetPrimaryAppearances instead (zero allocation).
    if colorFilter and ETB.ColorSearch and ETB.ColorSearch:HasData() then
        local hasMatch = false
        
        -- Community sets: check items directly (already have visualIDs)
        if setData.items then
            for _, item in pairs(setData.items) do
                local vid = nil
                if type(item) == "table" then
                    vid = item.visualID or item[2]
                end
                if vid and ETB.ColorSearch:ItemMatchesCategory(vid, nil, colorFilter) then
                    hasMatch = true
                    break
                end
            end
        end
        
        -- Blizzard sets: lookup in pre-built _setColorCache (built at start of RefreshSetList)
        if not hasMatch and setData.setID and SetsUI._setColorCache then
            local cachedColors = SetsUI._setColorCache[setData.setID]
            if cachedColors then
                local cat = ETB.ColorSearch.CategoryByID and ETB.ColorSearch.CategoryByID[colorFilter]
                if cat and cachedColors[cat.idx] then
                    hasMatch = true
                end
            end
        end
        
        -- No items data at all = pass through
        if not setData.items and not setData.setID then
            hasMatch = true
        end
        if not hasMatch then return false end
    end
    
    return true
end

function SetsUI:RefreshSetList()
    if not self.setListFrame or not ETB.SetsData then return end
    
    -- Get sets from SetsData
    if ETB.SetsData:CountTable(ETB.SetsData.allSets) == 0 then
        ETB.SetsData:DiscoverSets()
    end
    
    -- Wipe and reuse existing sortedSets table (avoids orphaning old entries)
    if self.sortedSets then
        wipe(self.sortedSets)
    else
        self.sortedSets = {}
    end
    
    -- Reusable filter data table (avoids creating 900 anonymous tables per refresh)
    local filterData = self._filterData
    if not filterData then
        filterData = {}
        self._filterData = filterData
    end
    
    -- Completion cache: only rebuild when TRANSMOG_COLLECTION_UPDATED fires
    if not self._completionCache then
        self._completionCache = {}
    end
    
    -- Pre-build Blizzard set color cache (ONCE, persists until collection update)
    -- Uses GetAllSourceIDs → GetSourceInfo → sourceInfo.visualID → ColorMap
    -- All lookups after first build are pure table reads (zero allocation)
    if self.colorFilter and not self._setColorCache and ETB.ColorSearch and ETB.ColorSearch:HasData() then
        self._setColorCache = {}
        local colorMap = ETB.ColorMap
        if colorMap then
            for setID in pairs(ETB.SetsData.baseSets) do
                local colorSet = {}
                local ok, sourceIDs = pcall(C_TransmogSets.GetAllSourceIDs, setID)
                if ok and sourceIDs then
                    for _, sid in ipairs(sourceIDs) do
                        local siOK, si = pcall(C_TransmogCollection.GetSourceInfo, sid)
                        if siOK and si and si.visualID then
                            local packed = colorMap[si.visualID]
                            if packed then
                                -- Direct decode into set — no temporary tables
                                if packed <= 12 then
                                    colorSet[packed] = true
                                elseif packed <= 1299 then
                                    colorSet[math.floor(packed / 100)] = true
                                    local s = packed % 100
                                    if s > 0 and s <= 12 then colorSet[s] = true end
                                else
                                    colorSet[math.floor(packed / 10000)] = true
                                    local rem = packed % 10000
                                    local s = math.floor(rem / 100)
                                    local t = rem % 100
                                    if s > 0 and s <= 12 then colorSet[s] = true end
                                    if t > 0 and t <= 12 then colorSet[t] = true end
                                end
                            end
                        end
                    end
                end
                self._setColorCache[setID] = colorSet
            end
        end
        ETB:Debug("SetsUI", "Built color cache for Blizzard sets")
    end
    
    -- Determine which sources to load based on filter
    local loadBlizzard = (self.currentFilter ~= "custom" and self.currentFilter ~= "community")
    local loadCustom = (self.currentFilter ~= "blizzard" and self.currentFilter ~= "community")
    local loadCommunity = (self.currentFilter ~= "blizzard" and self.currentFilter ~= "custom")
    
    local playerFaction = UnitFactionGroup("player")
    local primaryArmor = ETB.BlizzardData and ETB.BlizzardData:GetPlayerPrimaryArmor() or "CLOTH"
    
    -- ==========================================================================
    -- BLIZZARD SETS
    -- ==========================================================================
    if loadBlizzard then
        for setID, setInfo in pairs(ETB.SetsData.baseSets) do
            local include = true
            
            -- Use cached completion (rebuilt only on collection update events)
            local cachedComp = self._completionCache[setID]
            local collected, total
            if cachedComp then
                collected, total = cachedComp[1], cachedComp[2]
            else
                collected, total = ETB.SetsData:GetSetCompletion(setID)
                self._completionCache[setID] = { collected, total }
            end
            local isComplete = (collected == total and total > 0)
            local hasProgress = (collected > 0)
            
            -- Apply completion filters
            if self.currentFilter == "collected" then
                include = isComplete
            elseif self.currentFilter == "incomplete" then
                include = not isComplete
            end
            
            -- Apply search
            if include and self.searchText ~= "" then
                local nameLower = (setInfo.name or ""):lower()
                local labelLower = (setInfo.label or ""):lower()
                include = nameLower:find(self.searchText, 1, true) or labelLower:find(self.searchText, 1, true)
            end
            
            -- Apply dropdown filters (reuse filterData table)
            if include then
                filterData.armorType = ClassMaskToArmor(setInfo.classMask)
                filterData.expansionID = setInfo.expansionID
                filterData.classMask = setInfo.classMask
                filterData.setID = setID
                filterData.expansion = nil
                filterData.items = nil
                include = PassesDropdownFilters(filterData, self.armorFilter, self.expansionFilter, self.classFilter, self.colorFilter)
            end
            
            if include then
                table.insert(self.sortedSets, {
                    setID = setID,
                    name = setInfo.name or "Unknown",
                    label = setInfo.label,
                    collected = isComplete,
                    collectedSlots = collected,
                    totalSlots = total,
                    hasProgress = hasProgress,
                    classMask = setInfo.classMask,
                    faction = setInfo.requiredFaction,
                    isCustom = false,
                })
            end
        end
    end
    
    -- ==========================================================================
    -- CUSTOM SETS (unofficial dungeon sets, etc.)
    -- ==========================================================================
    if loadCustom and ETB.CustomSets then
        for setID, setInfo in pairs(ETB.CustomSets:GetAllSets()) do
            local include = true
            
            -- Get completion for custom set
            local collected, total = ETB.CustomSets:GetSetCompletion(setID)
            local isComplete = (collected == total and total > 0)
            local hasProgress = (collected > 0)
            
            -- Apply completion filters
            if self.currentFilter == "collected" then
                include = isComplete
            elseif self.currentFilter == "incomplete" then
                include = not isComplete
            end
            -- "all", "custom" = no additional filtering beyond source
            
            -- Apply search
            if include and self.searchText ~= "" then
                local nameLower = (setInfo.name or ""):lower()
                local sourceLower = (setInfo.source or ""):lower()
                include = nameLower:find(self.searchText, 1, true) or sourceLower:find(self.searchText, 1, true)
            end
            
            if include then
                table.insert(self.sortedSets, {
                    setID = setID,
                    name = setInfo.name or "Unknown",
                    label = setInfo.source,
                    collected = isComplete,
                    collectedSlots = collected,
                    totalSlots = total,
                    hasProgress = hasProgress,
                    classMask = nil,
                    faction = setInfo.faction,
                    isCustom = true,
                    armorType = setInfo.armorType,
                })
            end
        end
    end
    
    -- ==========================================================================
    -- COMMUNITY SETS
    -- ==========================================================================
    if loadCommunity and ETB.CommunitySets then
        if not ETB.CommunitySets.loaded then
            ETB.CommunitySets:LoadData()
        end
        
        for setID, setInfo in pairs(ETB.CommunitySets:GetAllSets()) do
            local include = true
            
            local collected = setInfo.collectedCount or 0
            local total = setInfo.pieceCount or 0
            local isComplete = (collected == total and total > 0)
            local hasProgress = (collected > 0)
            
            if self.currentFilter == "collected" then
                include = isComplete
            elseif self.currentFilter == "incomplete" then
                include = not isComplete
            end
            
            if include and self.searchText ~= "" then
                local nameLower = (setInfo.name or ""):lower()
                local sourceLower = (setInfo.source or ""):lower()
                local armorLower = (setInfo.armorDisplay or ""):lower()
                include = nameLower:find(self.searchText, 1, true)
                    or sourceLower:find(self.searchText, 1, true)
                    or armorLower:find(self.searchText, 1, true)
            end
            
            -- Apply dropdown filters (reuse filterData table)
            if include then
                filterData.armorType = setInfo.armorType
                filterData.expansion = setInfo.expansionName
                filterData.classMask = nil
                filterData.items = setInfo.items
                filterData.expansionID = nil
                filterData.setID = nil
                include = PassesDropdownFilters(filterData, self.armorFilter, self.expansionFilter, self.classFilter, self.colorFilter)
            end
            
            if include then
                table.insert(self.sortedSets, {
                    setID = setID,
                    name = setInfo.name or "Unknown",
                    label = setInfo.source,
                    collected = isComplete,
                    collectedSlots = collected,
                    totalSlots = total,
                    hasProgress = hasProgress,
                    classMask = nil,
                    faction = nil,
                    isCustom = false,
                    isCommunity = true,
                    armorType = setInfo.armorType,
                    expansion = setInfo.expansionName,
                    filterType = setInfo.filterName,
                })
            end
        end
    end
    
    -- Sort by completion (incomplete first for "missing" to be useful), then by name
    table.sort(self.sortedSets, function(a, b)
        -- If same completion status, sort by name
        if a.collected == b.collected then
            -- Within incomplete, show closest to completion first
            if not a.collected and a.totalSlots > 0 and b.totalSlots > 0 then
                local aProgress = a.collectedSlots / a.totalSlots
                local bProgress = b.collectedSlots / b.totalSlots
                if aProgress ~= bProgress then
                    return aProgress > bProgress  -- Higher progress first
                end
            end
            return a.name < b.name
        end
        -- Complete sets at the bottom when showing "All"
        return not a.collected
    end)
    
    -- Update stats with more useful info
    local completeSets = 0
    local inProgressSets = 0
    local blizzardCount = 0
    local customCount = 0
    
    for _, setData in ipairs(self.sortedSets) do
        if setData.collected then
            completeSets = completeSets + 1
        elseif setData.hasProgress then
            inProgressSets = inProgressSets + 1
        end
        if setData.isCustom then
            customCount = customCount + 1
        else
            blizzardCount = blizzardCount + 1
        end
    end
    
    local statsText = string.format("|cff888888%d sets", #self.sortedSets)
    if self.currentFilter == "blizzard" then
        statsText = statsText .. string.format(" (|cff00ff00%d complete|r)|r", completeSets)
    elseif self.currentFilter == "custom" then
        statsText = statsText .. string.format(" (|cffffd100★ custom|r)|r")
    elseif self.currentFilter == "community" then
        statsText = statsText .. string.format(" (|cff00ff00%d done|r, |cff00ccff◆ community|r)|r", completeSets)
    else
        statsText = statsText .. string.format(" (|cff00ff00%d done|r, |cffffd100%d in progress|r)|r", completeSets, inProgressSets)
    end
    self.setListFrame.stats:SetText(statsText)
    
    -- Populate scroll list
    self:PopulateSetList()
    self:UpdateFilterButtons()
    
    -- Step GC to reclaim temporary tables from this refresh cycle
    collectgarbage("step", 200)
end

-- Virtual scroll constants
local BUTTON_HEIGHT = 28
local BUTTON_POOL_SIZE = 25  -- Enough for visible area + buffer

function SetsUI:PopulateSetList()
    local scrollChild = self.setListFrame.scrollChild
    local scrollFrame = self.setListFrame.scrollFrame
    if not scrollChild or not scrollFrame then return end
    
    -- Create button pool once (only first call creates them)
    if #self.setButtons < BUTTON_POOL_SIZE then
        for i = #self.setButtons + 1, BUTTON_POOL_SIZE do
            local btn = self:CreateSetListButton(scrollChild)
            self.setButtons[i] = btn
        end
    end
    
    -- Hide all buttons initially
    for _, btn in ipairs(self.setButtons) do
        btn:Hide()
    end
    
    local totalSets = #self.sortedSets
    
    -- Set scroll child height to full virtual height (makes scrollbar correct)
    scrollChild:SetHeight(math.max(totalSets * BUTTON_HEIGHT, 1))
    
    -- Hook scroll to update visible buttons on scroll change
    scrollFrame:SetScript("OnVerticalScroll", function(sf, offset)
        -- Standard UIPanelScrollFrame handler
        local scrollBar = sf.ScrollBar or _G[sf:GetName() .. "ScrollBar"]
        if scrollBar then
            scrollBar:SetValue(offset)
        end
        SetsUI:UpdateVisibleButtons()
    end)
    
    -- Initial render
    self:UpdateVisibleButtons()
end

function SetsUI:UpdateVisibleButtons()
    local scrollChild = self.setListFrame.scrollChild
    local scrollFrame = self.setListFrame.scrollFrame
    if not scrollChild or not scrollFrame then return end
    
    local totalSets = #self.sortedSets
    if totalSets == 0 then
        for _, btn in ipairs(self.setButtons) do btn:Hide() end
        return
    end
    
    -- Get current scroll offset
    local scrollOffset = scrollFrame:GetVerticalScroll() or 0
    local visibleHeight = scrollFrame:GetHeight() or 400
    
    -- Calculate which data indices are visible
    local firstVisible = math.floor(scrollOffset / BUTTON_HEIGHT) + 1
    local lastVisible = math.min(firstVisible + BUTTON_POOL_SIZE - 1, totalSets)
    firstVisible = math.max(1, firstVisible)
    
    -- Hide all buttons first
    for _, btn in ipairs(self.setButtons) do
        btn:Hide()
    end
    
    -- Assign data to pool buttons
    local poolIndex = 0
    for dataIndex = firstVisible, lastVisible do
        poolIndex = poolIndex + 1
        if poolIndex > BUTTON_POOL_SIZE then break end
        
        local btn = self.setButtons[poolIndex]
        if not btn then break end
        
        local setData = self.sortedSets[dataIndex]
        if not setData then break end
        
        -- Position at absolute Y based on data index
        local yPos = -((dataIndex - 1) * BUTTON_HEIGHT)
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, yPos)
        btn:SetPoint("RIGHT", scrollChild, "RIGHT", -5, 0)
        btn:SetHeight(BUTTON_HEIGHT)
        
        -- Update button content
        self:UpdateSetButton(btn, setData)
        
        btn.setID = setData.setID
        btn:Show()
    end
end

function SetsUI:UpdateSetButton(btn, setData)
    local collected = setData.collectedSlots or 0
    local total = setData.totalSlots or 0
    local isComplete = setData.collected
    
    -- Build display name
    local nameColor = isComplete and "|cff00ff00" or "|cffffffff"
    local displayName = nameColor .. setData.name .. "|r"
    
    if setData.classMask then
        local className = GetClassFromMask(setData.classMask)
        if className then
            local classColor = CLASS_COLORS[className] or "|cffaaaaaa"
            displayName = displayName .. " " .. classColor .. "(" .. className .. ")|r"
        end
    end
    
    if setData.faction then
        local factionDisplay = GetFactionDisplay(setData.faction)
        if factionDisplay then
            displayName = displayName .. " " .. factionDisplay
        end
    end
    
    if setData.isCustom then
        displayName = "|cffffd100★|r " .. displayName
    end
    
    if setData.isCommunity then
        displayName = "|cff00ccff◆|r " .. displayName
        if setData.armorType then
            local armorColors = {
                CLOTH = "|cffff7dff", LEATHER = "|cff00ff00",
                MAIL = "|cff0088ff", PLATE = "|cffffcc00",
                COSMETIC = "|cffff8800",
            }
            local armorShort = {
                CLOTH = "Cloth", LEATHER = "Leather",
                MAIL = "Mail", PLATE = "Plate",
                COSMETIC = "Cosmetic",
            }
            local color = armorColors[setData.armorType] or "|cffaaaaaa"
            local label = armorShort[setData.armorType] or setData.armorType
            displayName = displayName .. " " .. color .. "(" .. label .. ")|r"
        end
    end
    
    btn.name:SetText(displayName)
    
    -- Completion text with color
    if total > 0 then
        local compColor = isComplete and "|cff00ff00" or (collected > 0 and "|cffffd100" or "|cffaaaaaa")
        btn.completion:SetText(compColor .. collected .. "/" .. total .. "|r")
        
        local progress = collected / total
        local barWidth = btn:GetWidth() - 10
        if barWidth <= 0 then barWidth = 200 end
        btn.progressBar:SetWidth(math.max(1, barWidth * progress))
        
        if isComplete then
            btn.progressBar:SetColorTexture(0.2, 0.8, 0.2, 1)
        elseif progress > 0.5 then
            btn.progressBar:SetColorTexture(0.6, 0.6, 0.2, 1)
        elseif progress > 0 then
            btn.progressBar:SetColorTexture(0.4, 0.4, 0.4, 1)
        else
            btn.progressBar:SetColorTexture(0.2, 0.2, 0.2, 1)
        end
        btn.progressBar:Show()
        btn.progressBg:Show()
    else
        btn.completion:SetText("")
        btn.progressBar:Hide()
        btn.progressBg:Hide()
    end
    
    -- Highlight selected
    if setData.setID == self.selectedSetID then
        btn.bg:SetColorTexture(0.2, 0.4, 0.6, 0.7)
    else
        btn.bg:SetColorTexture(0.1, 0.1, 0.1, 0.5)
    end
end

function SetsUI:CreateSetListButton(parent)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetHeight(28)  -- Slightly taller
    btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
    
    btn.bg = btn:CreateTexture(nil, "BACKGROUND")
    btn.bg:SetAllPoints()
    btn.bg:SetColorTexture(0.1, 0.1, 0.1, 0.5)
    
    btn.name = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    btn.name:SetPoint("TOPLEFT", 5, -2)
    btn.name:SetPoint("RIGHT", btn, "RIGHT", -45, 0)  -- Leave room for completion text
    btn.name:SetJustifyH("LEFT")
    btn.name:SetWordWrap(false)  -- Prevent overflow
    
    -- Completion text (e.g., "3/8")
    btn.completion = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    btn.completion:SetPoint("TOPRIGHT", -5, -2)
    btn.completion:SetWidth(40)  -- Fixed width for completion
    btn.completion:SetJustifyH("RIGHT")
    
    -- Progress bar background
    btn.progressBg = btn:CreateTexture(nil, "BORDER")
    btn.progressBg:SetPoint("BOTTOMLEFT", 5, 3)
    btn.progressBg:SetPoint("BOTTOMRIGHT", -5, 3)
    btn.progressBg:SetHeight(4)
    btn.progressBg:SetColorTexture(0.15, 0.15, 0.15, 1)
    
    -- Progress bar fill
    btn.progressBar = btn:CreateTexture(nil, "ARTWORK")
    btn.progressBar:SetPoint("BOTTOMLEFT", 5, 3)
    btn.progressBar:SetHeight(4)
    btn.progressBar:SetColorTexture(0.2, 0.6, 0.2, 1)
    
    btn:SetScript("OnClick", function(self)
        SetsUI:SelectSet(self.setID)
    end)
    
    return btn
end

-- =============================================================================
-- SET DETAILS (Blizzard Style)
-- =============================================================================

function SetsUI:SelectSet(setID)
    self.selectedSetID = setID
    
    -- Refresh list to update highlighting
    self:PopulateSetList()
    
    local detail = self.setDetailFrame
    if not detail then return end
    
    -- Check if this is a custom set first (custom sets have string IDs starting with CUSTOM_, USER_, or SB_)
    local isCustomSet = false
    local isCommunitySet = false
    if type(setID) == "string" then
        isCustomSet = (setID:find("^CUSTOM_") ~= nil) or (setID:find("^USER_") ~= nil) or (setID:find("^SB_") ~= nil)
        isCommunitySet = (setID:find("^COMMUNITY_") ~= nil)
    end
    
    if isCommunitySet and ETB.CommunitySets then
        self:DisplayCommunitySet(setID, detail)
        return
    end
    
    if isCustomSet and ETB.CustomSets then
        -- Handle CUSTOM SET
        self:DisplayCustomSet(setID, detail)
        return
    end
    
    -- Handle BLIZZARD SET
    local setInfo = ETB.SetsData:GetSetInfo(setID)
    if not setInfo then
        detail.setName:SetText("|cffff0000Set not found|r")
        return
    end
    
    -- Store current setID
    self.currentDisplayedSetID = setID
    
    -- Hide delete button for Blizzard sets (can't delete official sets)
    if detail.deleteSetBtn then
        detail.deleteSetBtn:Hide()
    end
    
    -- Update set name (gold)
    detail.setName:SetText(setInfo.name or "Unknown")
    
    -- Update source (from label + description)
    local sourceText = setInfo.label or ""
    if setInfo.description and setInfo.description ~= "" then
        sourceText = sourceText .. " - " .. setInfo.description
    end
    detail.sourceName:SetText(sourceText)
    
    -- Update completion using slot-based counting
    local collected, total = ETB.SetsData:GetSetCompletion(setID)
    local slotItems = ETB.SetsData:GetSetItemsBySlot(setID)
    
    if total > 0 then
        local remaining = total - collected
        if collected == total then
            detail.completion:SetText("|cff00ff00✓ COMPLETE! (" .. total .. "/" .. total .. " slots)|r")
        elseif remaining == 1 then
            detail.completion:SetText("|cffffd100" .. collected .. "/" .. total .. " slots|r |cffff8800(1 more needed!)|r")
        else
            detail.completion:SetText("|cffffd100" .. collected .. "/" .. total .. " slots|r |cff888888(" .. remaining .. " more needed)|r")
        end
    else
        -- Fallback to API-provided completion
        local apiCollected, apiTotal = ETB.SetsData:GetSetCompletion(setID)
        if apiTotal > 0 then
            local color = apiCollected == apiTotal and "|cff00ff00" or "|cffffd100"
            detail.completion:SetText(color .. apiCollected .. " / " .. apiTotal .. " Collected|r")
        else
            detail.completion:SetText("|cff888888Completion unknown|r")
        end
    end
    
    -- Update variant dropdown
    self:PopulateVariantDropdown(setID, setInfo)
    
    -- Update piece icons
    self:UpdatePieceIcons(setID)
    
    -- Dress up the model with the set items - order is important!
    -- 1. First set the unit (this loads the player model with their gear)
    detail.model:SetUnit("player")
    detail.model:SetRotation(0)
    detail.model:SetPosition(0, 0, 0)
    
    -- 2. Then undress to remove all gear
    detail.model:Undress()
    
    -- 3. Try on one item from each slot (prefer collected items)
    for _, slotData in ipairs(slotItems) do
        local itemToTry = nil
        
        -- First try to find a collected item
        if slotData.primary and slotData.primary.collected then
            itemToTry = slotData.primary
        else
            -- Check alternatives for a collected one
            if slotData.alternatives then
                for _, alt in ipairs(slotData.alternatives) do
                    if alt.collected then
                        itemToTry = alt
                        break
                    end
                end
            end
        end
        
        -- If no collected item, just use the primary
        if not itemToTry and slotData.primary then
            itemToTry = slotData.primary
        end
        
        -- Try it on
        if itemToTry and itemToTry.itemID then
            pcall(function()
                detail.model:TryOn("item:" .. itemToTry.itemID)
            end)
        end
    end
end

-- =============================================================================
-- CUSTOM SET DISPLAY
-- =============================================================================

function SetsUI:DisplayCustomSet(setID, detail)
    if ETB.debug then print("|cff00ffff[ETB Display Debug]|r === DisplayCustomSet called ===") end
    if ETB.debug then print("|cff00ffff[ETB Display Debug]|r setID: " .. tostring(setID)) end
    
    local customSet = ETB.CustomSets:GetSet(setID)
    if not customSet then
        if ETB.debug then print("|cffff0000[ETB Display Debug]|r GetSet returned nil!") end
        detail.setName:SetText("|cffff0000Custom set not found|r")
        return
    end
    
    if ETB.debug then print("|cff00ff00[ETB Display Debug]|r Set found: " .. tostring(customSet.name)) end
    if ETB.debug then print("|cff00ffff[ETB Display Debug]|r armorType: " .. tostring(customSet.armorType)) end
    if ETB.debug then print("|cff00ffff[ETB Display Debug]|r source: " .. tostring(customSet.source)) end
    
    -- Debug items
    if customSet.items then
        if ETB.debug then print("|cff00ffff[ETB Display Debug]|r Items table exists") end
        local count = 0
        for slot, itemID in pairs(customSet.items) do
            count = count + 1
            if ETB.debug then print(string.format("|cff00ffff[ETB Display Debug]|r   Slot %s = ItemID %s", tostring(slot), tostring(itemID))) end
        end
        if ETB.debug then print("|cff00ffff[ETB Display Debug]|r Total items: " .. count) end
    else
        if ETB.debug then print("|cffff0000[ETB Display Debug]|r Items table is NIL!") end
    end
    
    -- Store current setID for delete function
    self.currentDisplayedSetID = setID
    
    -- Update set name with star marker
    detail.setName:SetText("|cffffd100★|r " .. (customSet.name or "Unknown"))
    
    -- Update source
    local sourceText = customSet.source or "Unknown Source"
    if customSet.armorType then
        sourceText = sourceText .. " |cff888888(" .. customSet.armorType .. ")|r"
    end
    detail.sourceName:SetText(sourceText)
    
    -- Get completion
    local collected, total = ETB.CustomSets:GetSetCompletion(setID)
    if ETB.debug then print(string.format("|cff00ffff[ETB Display Debug]|r GetSetCompletion returned: %d/%d", collected, total)) end
    
    if total > 0 then
        local remaining = total - collected
        if collected == total then
            detail.completion:SetText("|cff00ff00✓ COMPLETE! (" .. total .. "/" .. total .. " pieces)|r")
        elseif remaining == 1 then
            detail.completion:SetText("|cffffd100" .. collected .. "/" .. total .. " pieces|r |cffff8800(1 more needed!)|r")
        else
            detail.completion:SetText("|cffffd100" .. collected .. "/" .. total .. " pieces|r |cff888888(" .. remaining .. " more needed)|r")
        end
    else
        detail.completion:SetText("|cff888888No items defined|r")
    end
    
    -- Hide variant dropdown for custom sets
    if detail.variantDropdown then
        ETBDropdown:SetText(detail.variantDropdown, "N/A")
    end
    
    -- Show delete button only for user-defined sets (not predefined ones)
    if detail.deleteSetBtn then
        if customSet.isUserDefined or setID:find("^SB_") or setID:find("^USER_") then
            detail.deleteSetBtn:Show()
        else
            detail.deleteSetBtn:Hide()
        end
    end
    
    -- Update piece icons for custom set
    if ETB.debug then print("|cff00ffff[ETB Display Debug]|r Calling UpdateCustomSetPieceIcons...") end
    self:UpdateCustomSetPieceIcons(setID, customSet)
    
    -- Dress up the model - order is important!
    -- 1. First set the unit (this loads the player model with their gear)
    detail.model:SetUnit("player")
    detail.model:SetRotation(0)
    detail.model:SetPosition(0, 0, 0)
    
    -- 2. Then undress to remove all gear
    detail.model:Undress()
    
    -- 3. Try on each item from the custom set
    if customSet.items then
        for slot, itemID in pairs(customSet.items) do
            if itemID and type(itemID) == "number" then
                pcall(function()
                    detail.model:TryOn("item:" .. itemID)
                end)
            end
        end
    end
end

-- =============================================================================
-- COMMUNITY SET DISPLAY
-- =============================================================================

function SetsUI:DisplayCommunitySet(setID, detail)
    local communitySet = ETB.CommunitySets:GetSet(setID)
    if not communitySet then
        detail.setName:SetText("|cffff0000Community set not found|r")
        return
    end
    
    self.currentDisplayedSetID = setID
    
    -- Hide delete button (can't delete community sets)
    if detail.deleteSetBtn then
        detail.deleteSetBtn:Hide()
    end
    
    -- Set name with community marker (same style as custom ★)
    detail.setName:SetText("|cff00ccff◆|r " .. (communitySet.name or "Unknown"))
    
    -- Source line — expansion + filter type + armor type (same format as custom sets)
    local sourceText = communitySet.expansionName or ""
    if communitySet.filterName and communitySet.filterName ~= "" then
        sourceText = sourceText .. " " .. communitySet.filterName
    end
    if communitySet.armorDisplay then
        sourceText = sourceText .. " |cff888888(" .. communitySet.armorDisplay .. ")|r"
    end
    detail.sourceName:SetText(sourceText)
    
    -- Completion — same format as custom sets
    local collected, total = ETB.CommunitySets:GetSetCompletion(setID)
    if total > 0 then
        local remaining = total - collected
        if collected == total then
            detail.completion:SetText("|cff00ff00✓ COMPLETE! (" .. total .. "/" .. total .. " pieces)|r")
        elseif remaining == 1 then
            detail.completion:SetText("|cffffd100" .. collected .. "/" .. total .. " pieces|r |cffff8800(1 more needed!)|r")
        else
            detail.completion:SetText("|cffffd100" .. collected .. "/" .. total .. " pieces|r |cff888888(" .. remaining .. " more needed)|r")
        end
    else
        detail.completion:SetText("|cff888888No items defined|r")
    end
    
    -- Hide variant dropdown (community sets have no variants)
    if detail.variantDropdown then
        ETBDropdown:SetText(detail.variantDropdown, "N/A")
    end
    
    -- Update piece icons (same treatment as custom sets)
    self:UpdateCommunitySetPieceIcons(setID, communitySet)
    
    -- Dress model with ALL items (including weapons, back, tabard — not just armor)
    detail.model:SetUnit("player")
    detail.model:SetRotation(0)
    detail.model:SetPosition(0, 0, 0)
    detail.model:Undress()
    
    -- Pre-cache all items and dress model
    local uncachedCount = 0
    for invSlot, item in pairs(communitySet.items) do
        if item.itemID and item.itemID > 0 then
            pcall(function()
                detail.model:TryOn("item:" .. item.itemID)
            end)
            -- Trigger cache request for uncached items
            local name = GetItemInfo(item.itemID)
            if not name then
                uncachedCount = uncachedCount + 1
            end
        end
    end
    
    -- If items were uncached, schedule a delayed refresh so icons/tooltips update
    if uncachedCount > 0 and C_Timer and C_Timer.After then
        local capturedSetID = setID
        local capturedSet = communitySet
        C_Timer.After(0.5, function()
            -- Only refresh if still viewing the same set
            if SetsUI.currentDisplayedSetID == capturedSetID then
                SetsUI:UpdateCommunitySetPieceIcons(capturedSetID, capturedSet)
            end
        end)
        -- Second retry for slow network
        C_Timer.After(2.0, function()
            if SetsUI.currentDisplayedSetID == capturedSetID then
                SetsUI:UpdateCommunitySetPieceIcons(capturedSetID, capturedSet)
            end
        end)
    end
end

function SetsUI:UpdateCommunitySetPieceIcons(setID, communitySet)
    local pieceButtons = self.pieceButtons
    if not pieceButtons then return end
    
    -- COMPREHENSIVE RESET
    for i, btn in ipairs(pieceButtons) do
        btn.icon:SetTexture("Interface\\PaperDoll\\UI-Backpack-EmptySlot")
        btn.icon:SetDesaturated(false)
        btn.border:SetVertexColor(0.3, 0.3, 0.3)
        btn.check:Hide()
        btn.missing:Hide()
        btn.desaturate:Hide()
        btn.cycleBtn:Hide()
        btn.itemID = nil
        btn.slotData = nil
        btn.allItems = nil
        btn.alternatives = nil
        btn.currentIndex = nil
        btn.isCollected = false
        btn.currentItemCollected = false
        btn.primaryName = nil
        btn.primaryCollected = nil
        btn.invType = nil
        if btn.altCount then btn.altCount:SetText("") end
        
        local slotName = INVSLOT_NAMES[BUTTON_TO_INVSLOT[i]] or ""
        if btn.nameLabel then
            btn.nameLabel:SetText(slotName)
            btn.nameLabel:SetTextColor(0.6, 0.6, 0.6)
        end
        
        local capturedSlotName = slotName
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText(capturedSlotName or "Empty Slot", 0.5, 0.5, 0.5)
            GameTooltip:AddLine("No item in this slot", 0.5, 0.5, 0.5)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    end
    
    if not communitySet or not communitySet.items then return end
    
    -- Populate each button — map invSlot → button via INVSLOT_TO_BUTTON
    for invSlot, item in pairs(communitySet.items) do
        local btnIdx = INVSLOT_TO_BUTTON[invSlot]
        if not btnIdx then
            -- Check for robe (slot 20 → chest button)
            if invSlot == 20 then btnIdx = 3 end
        end
        
        local btn = btnIdx and pieceButtons[btnIdx] or nil
        
        if btn and item and item.itemID then
            btn.itemID = item.itemID
            
            -- Get icon — prefer resolved data, fallback to API
            local iconTexture = item.icon
            if not iconTexture or type(iconTexture) ~= "number" then
                local _, _, _, _, iconPath = GetItemInfoInstant(item.itemID)
                iconTexture = iconPath
            end
            if not iconTexture then
                local _, _, _, _, _, _, _, _, _, fallbackIcon = GetItemInfo(item.itemID)
                iconTexture = fallbackIcon
            end
            btn.icon:SetTexture(iconTexture or "Interface\\Icons\\INV_Misc_QuestionMark")
            
            -- Collection status — check live via appearance sources (pre-resolved may be stale/pending)
            local isCollected = item.isCollected or false
            if not isCollected and item.itemID then
                -- Live check: itemID → appearanceID → sources → isCollected
                if C_TransmogCollection and C_TransmogCollection.GetItemInfo then
                    local ok, appID = pcall(C_TransmogCollection.GetItemInfo, item.itemID)
                    if ok and appID and C_TransmogCollection.GetAppearanceSources then
                        local srcOK, sources = pcall(C_TransmogCollection.GetAppearanceSources, appID)
                        if srcOK and sources then
                            for _, src in ipairs(sources) do
                                local sid = type(src) == "table" and src.sourceID or src
                                if sid then
                                    local siOK, si = pcall(C_TransmogCollection.GetSourceInfo, sid)
                                    if siOK and si and si.isCollected then
                                        isCollected = true
                                        item.isCollected = true  -- Update cached state
                                        break
                                    end
                                end
                            end
                        end
                    end
                end
            end
            
            btn.isCollected = isCollected
            btn.currentItemCollected = isCollected
            
            -- Slot label with color (same as custom sets)
            local slotDisplayName = INVSLOT_NAMES[invSlot] or "Slot"
            if btn.nameLabel then
                btn.nameLabel:SetText(slotDisplayName)
                if isCollected then
                    btn.nameLabel:SetTextColor(0.5, 1, 0.5)  -- Green
                else
                    btn.nameLabel:SetTextColor(1, 0.5, 0.5)  -- Red
                end
            end
            
            -- Find alternatives via appearance sources (same as custom sets)
            local alternatives = {}
            local resolvedVID = ETB:ResolveVisualID(item)
            if resolvedVID and C_TransmogCollection then
                local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, resolvedVID)
                if ok and sources then
                    for _, source in ipairs(sources) do
                        local srcItemID = type(source) == "table" and source.itemID or nil
                        local srcCollected = type(source) == "table" and source.isCollected or false
                        local srcSourceID = type(source) == "table" and source.sourceID or nil
                        if srcItemID then
                            local altName, _, altQuality, _, _, _, _, _, _, altIcon = GetItemInfo(srcItemID)
                            if not altIcon then
                                _, _, _, _, altIcon = GetItemInfoInstant(srcItemID)
                            end
                            table.insert(alternatives, {
                                itemID = srcItemID,
                                sourceID = srcSourceID,
                                name = altName or ("Item " .. srcItemID),
                                icon = altIcon,
                                quality = altQuality or 2,
                                collected = srcCollected,
                            })
                        end
                    end
                end
            end
            
            -- Store alternatives and show cycle button if needed (same as custom sets)
            if #alternatives > 1 then
                btn.allItems = alternatives
                btn.currentIndex = 1
                btn.cycleBtn:Show()
            else
                btn.allItems = nil
                btn.cycleBtn:Hide()
            end
            
            -- Border + overlay (same colors as custom sets)
            if isCollected then
                btn.icon:SetDesaturated(false)
                btn.border:SetVertexColor(0, 1, 0)
                btn.check:Show()
                btn.desaturate:Hide()
            else
                btn.icon:SetDesaturated(true)
                btn.border:SetVertexColor(1, 0.5, 0)
                btn.missing:Show()
                btn.desaturate:Show()
            end
            
            -- Tooltip (same format as custom sets)
            local capturedItemID = item.itemID
            local capturedSlotName = slotDisplayName
            local capturedIsCollected = isCollected
            local altCount = btn.allItems and #btn.allItems or 0
            
            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
                if capturedItemID and capturedItemID > 0 then ETB:SafeSetTooltipItem(GameTooltip, capturedItemID) else GameTooltip:SetText(capturedSlotName or "Unknown Item") end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("|cffffd100" .. capturedSlotName .. "|r")
                
                if capturedIsCollected then
                    GameTooltip:AddLine("|cff00ff00✓ Slot Complete|r")
                else
                    GameTooltip:AddLine("|cffff4444✗ SLOT NEEDED|r")
                end
                
                if self.currentItemCollected then
                    GameTooltip:AddLine("|cff00ff00This item: Collected|r")
                else
                    GameTooltip:AddLine("|cffff6600This item: Not Collected|r")
                end
                
                if altCount > 1 then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine(string.format("|cffffd100%d items share this appearance|r", altCount))
                    GameTooltip:AddLine("|cff888888Click arrow to cycle alternatives|r")
                end
                
                GameTooltip:Show()
            end)
            btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end  -- if btn and item
    end
end

-- =============================================================================
-- DELETE CUSTOM SET
-- =============================================================================

function SetsUI:ConfirmDeleteSet()
    if not self.currentDisplayedSetID then
        ETB:Print("|cffff0000No set selected|r")
        return
    end
    
    local setID = self.currentDisplayedSetID
    local customSet = ETB.CustomSets and ETB.CustomSets:GetSet(setID)
    
    if not customSet then
        ETB:Print("|cffff0000Set not found|r")
        return
    end
    
    -- Check if this is a user-defined set (can delete) or predefined (cannot delete)
    if not customSet.isUserDefined and not setID:find("^SB_") and not setID:find("^USER_") then
        ETB:Print("|cffff0000Cannot delete predefined sets|r")
        return
    end
    
    -- Show delete confirmation (popup defined at load time)
    StaticPopup_Show("ETB_DELETE_CUSTOM_SET")
end

function SetsUI:DeleteCurrentSet()
    if not self.currentDisplayedSetID then return end
    
    local setID = self.currentDisplayedSetID
    local customSet = ETB.CustomSets and ETB.CustomSets:GetSet(setID)
    local setName = customSet and customSet.name or "Unknown"
    
    -- Remove from CustomSets
    if ETB.CustomSets and ETB.CustomSets.userSets then
        ETB.CustomSets.userSets[setID] = nil
        ETB.CustomSets:RebuildAllSets()
        ETB.CustomSets:SaveUserSets()
    end
    
    -- Clear selection
    self.currentDisplayedSetID = nil
    self.selectedSetID = nil
    
    -- Refresh the list
    self:PopulateSetList()
    
    -- Reset detail panel
    local detail = self.setDetailFrame
    if detail then
        detail.setName:SetText("Select a Set")
        detail.sourceName:SetText("")
        detail.completion:SetText("")
        if detail.deleteSetBtn then
            detail.deleteSetBtn:Hide()
        end
        detail.model:Undress()
    end
    
    ETB:Print(string.format("|cff00ff00Deleted custom set:|r %s", setName))
end

function SetsUI:UpdateCustomSetPieceIcons(setID, customSet)
    local pieceButtons = self.pieceButtons
    if not pieceButtons then return end
    
    -- Slot order for display (matches Blizzard's layout: Head/Shoulder/Chest/Waist/Legs/Feet/Wrist/Hands)
    local SLOT_ORDER_CUSTOM = {1, 2, 3, 4, 5, 6, 7, 8}
    local SLOT_NAMES_CUSTOM = {
        [1] = "Head", [2] = "Shoulders", [3] = "Chest", [4] = "Waist",
        [5] = "Legs", [6] = "Feet", [7] = "Wrists", [8] = "Hands", [9] = "Back"
    }
    
    -- COMPREHENSIVE RESET - Reset ALL buttons first to clean state
    for i, btn in ipairs(pieceButtons) do
        -- Reset icon to empty slot
        btn.icon:SetTexture("Interface\\PaperDoll\\UI-Backpack-EmptySlot")
        btn.icon:SetDesaturated(false)
        
        -- Reset border
        btn.border:SetVertexColor(0.3, 0.3, 0.3)  -- Gray for empty
        
        -- Hide all overlays
        btn.check:Hide()
        btn.missing:Hide()
        btn.desaturate:Hide()
        btn.cycleBtn:Hide()
        
        -- Clear all data
        btn.itemID = nil
        btn.slotData = nil
        btn.allItems = nil
        btn.alternatives = nil
        btn.currentIndex = nil
        btn.isCollected = false
        btn.currentItemCollected = false
        btn.primaryName = nil
        btn.primaryCollected = nil
        btn.invType = nil
        
        -- Reset name label to slot name
        if btn.nameLabel then
            btn.nameLabel:SetText(SLOT_NAMES_CUSTOM[SLOT_ORDER_CUSTOM[i]] or "")
            btn.nameLabel:SetTextColor(0.6, 0.6, 0.6)
        end
        
        -- Clear tooltips
        btn:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
            GameTooltip:SetText(SLOT_NAMES_CUSTOM[SLOT_ORDER_CUSTOM[i]] or "Empty Slot", 0.5, 0.5, 0.5)
            GameTooltip:AddLine("No item in this slot", 0.5, 0.5, 0.5)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    end
    
    -- Populate with custom set items
    if not customSet or not customSet.items then return end
    
    for i, slotIndex in ipairs(SLOT_ORDER_CUSTOM) do
        local btn = pieceButtons[i]
        if not btn then break end
        
        local itemID = customSet.items[slotIndex]
        if itemID and type(itemID) == "number" then
            btn.itemID = itemID
            
            -- Get item info (synchronous first, then async)
            local _, _, _, _, iconPath = GetItemInfoInstant(itemID)
            local name, link, quality, _, _, _, _, _, _, icon = GetItemInfo(itemID)
            
            -- Set icon
            btn.icon:SetTexture(icon or iconPath or "Interface\\Icons\\INV_Misc_QuestionMark")
            
            -- Check if collected via appearance source chain
            local isCollected = false
            if C_TransmogCollection and C_TransmogCollection.GetItemInfo then
                local ok, appID = pcall(C_TransmogCollection.GetItemInfo, itemID)
                if ok and appID and C_TransmogCollection.GetAppearanceSources then
                    local srcOK, sources = pcall(C_TransmogCollection.GetAppearanceSources, appID)
                    if srcOK and sources then
                        for _, src in ipairs(sources) do
                            local sid = type(src) == "table" and src.sourceID or src
                            if sid then
                                local siOK, si = pcall(C_TransmogCollection.GetSourceInfo, sid)
                                if siOK and si and si.isCollected then
                                    isCollected = true
                                    break
                                end
                            end
                        end
                    end
                end
            end
            
            btn.isCollected = isCollected
            btn.currentItemCollected = isCollected
            
            -- Update item name label - SHOW SLOT NAME, not item name
            if btn.nameLabel then
                local slotDisplayName = SLOT_NAMES_CUSTOM[slotIndex] or "Slot"
                btn.nameLabel:SetText(slotDisplayName)
                if isCollected then
                    btn.nameLabel:SetTextColor(0.5, 1, 0.5)  -- Green for collected
                else
                    btn.nameLabel:SetTextColor(1, 0.5, 0.5)  -- Red for needed
                end
            end
            
            -- Find alternatives (other items with same appearance)
            local alternatives = {}
            local appearanceID = ETB:ResolveVisualIDFromItemID(itemID)
            if appearanceID and C_TransmogCollection then
                local sources = C_TransmogCollection.GetAppearanceSources(appearanceID)
                if sources then
                    for _, source in ipairs(sources) do
                        if source.itemID then
                            local altName, _, altQuality, _, _, _, _, _, _, altIcon = GetItemInfo(source.itemID)
                            if not altIcon then
                                _, _, _, _, altIcon = GetItemInfoInstant(source.itemID)
                            end
                            table.insert(alternatives, {
                                itemID = source.itemID,
                                sourceID = source.sourceID,
                                name = altName or ("Item " .. source.itemID),
                                icon = altIcon,
                                quality = altQuality or 2,
                                collected = source.isCollected,
                            })
                        end
                    end
                end
            end
            
            -- Store alternatives and show cycle button if needed
            if #alternatives > 1 then
                btn.allItems = alternatives
                btn.currentIndex = 1
                btn.cycleBtn:Show()
            else
                btn.allItems = nil
                btn.cycleBtn:Hide()
            end
            
            -- Update visual state based on collection
            if isCollected then
                btn.icon:SetDesaturated(false)
                btn.border:SetVertexColor(0, 1, 0)  -- Green border
                btn.check:Show()
                btn.desaturate:Hide()
            else
                btn.icon:SetDesaturated(true)
                btn.border:SetVertexColor(1, 0.5, 0)  -- Orange border for needed
                btn.missing:Show()
                btn.desaturate:Show()
            end
            
            -- Enhanced tooltip with alternatives info
            local slotName = SLOT_NAMES_CUSTOM[slotIndex] or "Unknown Slot"
            local altCount = btn.allItems and #btn.allItems or 0
            local capturedItemID = itemID  -- Capture for closure
            
            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
                if capturedItemID and capturedItemID > 0 then ETB:SafeSetTooltipItem(GameTooltip, capturedItemID) else GameTooltip:SetText(slotName or "Unknown Item") end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("|cffffd100" .. slotName .. "|r")
                
                if self.isCollected then
                    GameTooltip:AddLine("|cff00ff00✓ Slot Complete|r")
                else
                    GameTooltip:AddLine("|cffff4444✗ SLOT NEEDED|r")
                end
                
                if self.currentItemCollected then
                    GameTooltip:AddLine("|cff00ff00This item: Collected|r")
                else
                    GameTooltip:AddLine("|cffff6600This item: Not Collected|r")
                end
                
                if altCount > 1 then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine(string.format("|cffffd100%d items share this appearance|r", altCount))
                    GameTooltip:AddLine("|cff888888Click arrow to cycle alternatives|r")
                end
                
                GameTooltip:Show()
            end)
            btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end
    end
end

function SetsUI:PopulateVariantDropdown(setID, setInfo)
    local detail = self.setDetailFrame
    if not detail or not detail.variantDropdown then return end
    
    -- Get base set ID if this is a variant
    local baseSetID = setID
    if setInfo and not setInfo.isBaseSet then
        local baseID = ETB.SetsData:GetBaseSetID(setID)
        if baseID then
            baseSetID = baseID
        end
    end
    
    -- Get all variants for this base set
    local variants = ETB.SetsData:GetVariantSets(baseSetID)
    
    -- Include base set in list
    local allSets = {{setID = baseSetID, isBase = true}}
    if type(variants) == "table" then
        for _, variantID in ipairs(variants) do
            if type(variantID) == "number" then
                table.insert(allSets, {setID = variantID, isBase = false})
            elseif type(variantID) == "table" and variantID.setID then
                table.insert(allSets, {setID = variantID.setID, isBase = false})
            end
        end
    end
    
    -- Find current variant name
    local currentName = setInfo and setInfo.description or "Normal"
    ETBDropdown:SetText(detail.variantDropdown, currentName)
    
    -- Initialize dropdown
    ETBDropdown:Initialize(detail.variantDropdown, function(self, _items_)
        for _, setData in ipairs(allSets) do
            local variantInfo = ETB.SetsData:GetSetInfo(setData.setID)
            if variantInfo then
                local info = ETBDropdown:CreateInfo()
                info.text = variantInfo.description or (setData.isBase and "Normal" or "Variant")
                info.value = setData.setID
                info.checked = (setData.setID == setID)
                info.func = function()
                    SetsUI:SelectSet(setData.setID)
                end
                ETBDropdown:AddButton(_items_, info)
            end
        end
    end)
end

function SetsUI:SetFilter(filter)
    self.currentFilter = filter
    self:RefreshSetList()
end

-- =============================================================================
-- SET SOURCES MODAL
-- =============================================================================

function SetsUI:ShowSetSources()
    if not self.selectedSetID then return end
    
    local setID = self.selectedSetID
    local setInfo = ETB.SetsData:GetSetInfo(setID)
    if not setInfo then return end
    
    -- Create modal if it doesn't exist
    if not self.sourcesModal then
        self:CreateSourcesModal()
    end
    
    -- Populate with set sources
    self:PopulateSourcesModal(setID, setInfo)
    self.sourcesModal:Show()
end

function SetsUI:CreateSourcesModal()
    local modal = CreateFrame("Frame", "ETBSetSourcesModal", UIParent, "BackdropTemplate")
    modal:SetSize(450, 500)
    modal:SetPoint("CENTER")
    modal: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 }
    })
    modal:SetMovable(true)
    modal:EnableMouse(true)
    modal:RegisterForDrag("LeftButton")
    modal:SetScript("OnDragStart", modal.StartMoving)
    modal:SetScript("OnDragStop", modal.StopMovingOrSizing)
    modal:SetFrameStrata("DIALOG")
    modal:Hide()
    
    -- Title
    modal.title = modal:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    modal.title:SetPoint("TOP", 0, -15)
    modal.title:SetText("Set Sources")
    
    -- Close button
    modal.closeBtn = CreateFrame("Button", nil, modal, "UIPanelCloseButton")
    modal.closeBtn:SetPoint("TOPRIGHT", -5, -5)
    
    -- Scroll frame for sources
    modal.scrollFrame = CreateFrame("ScrollFrame", nil, modal, "UIPanelScrollFrameTemplate")
    modal.scrollFrame:SetPoint("TOPLEFT", 15, -45)
    modal.scrollFrame:SetPoint("BOTTOMRIGHT", -35, 15)
    
    modal.scrollChild = CreateFrame("Frame", nil, modal.scrollFrame)
    modal.scrollChild:SetSize(400, 1)
    modal.scrollFrame:SetScrollChild(modal.scrollChild)
    
    modal.slotFrames = {}
    
    self.sourcesModal = modal
end

function SetsUI:PopulateSourcesModal(setID, setInfo)
    local modal = self.sourcesModal
    if not modal then return end
    
    -- Get completion info
    local collected, total = ETB.SetsData:GetSetCompletion(setID)
    local remaining = total - collected
    
    -- Update title with completion info
    if remaining == 0 then
        modal.title:SetText("|cff00ff00" .. setInfo.name .. " - COMPLETE!|r")
    else
        modal.title:SetText(setInfo.name .. " |cffffd100(" .. remaining .. " slots needed)|r")
    end
    
    -- Clear existing content
    for _, frame in ipairs(modal.slotFrames) do
        frame:Hide()
        if frame.items then
            for _, item in ipairs(frame.items) do
                item:Hide()
            end
        end
    end
    
    -- Get detailed slot info
    local slotDetails = ETB.SetsData:GetSlotCompletionDetails(setID)
    
    local yOffset = 0
    local slotIndex = 0
    
    for _, slot in ipairs(slotDetails) do
        slotIndex = slotIndex + 1
        
        -- Create slot frame if needed
        local slotFrame = modal.slotFrames[slotIndex]
        if not slotFrame then
            slotFrame = CreateFrame("Frame", nil, modal.scrollChild, "BackdropTemplate")
            slotFrame:SetSize(390, 20)
            modal.slotFrames[slotIndex] = slotFrame
            
            -- Background for incomplete slots
            slotFrame.bg = slotFrame:CreateTexture(nil, "BACKGROUND")
            slotFrame.bg:SetAllPoints()
            
            -- Slot header
            slotFrame.header = slotFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            slotFrame.header:SetPoint("TOPLEFT", 5, -3)
            
            -- Items container
            slotFrame.items = {}
        end
        
        slotFrame:SetPoint("TOPLEFT", 5, -yOffset)
        
        -- Set background based on completion
        if slot.isComplete then
            slotFrame.bg:SetColorTexture(0.1, 0.3, 0.1, 0.3)  -- Green tint
        else
            slotFrame.bg:SetColorTexture(0.3, 0.1, 0.1, 0.3)  -- Red tint - needs attention!
        end
        
        -- Slot header with clear status
        local statusIcon = slot.isComplete and "|cff00ff00✓|r" or "|cffff4444✗ NEED:|r"
        local optionsText = ""
        if not slot.isComplete and slot.totalOptions > 1 then
            optionsText = " |cff888888(any of " .. slot.totalOptions .. " items)|r"
        end
        slotFrame.header:SetText(statusIcon .. " |cffffd100" .. slot.slotName .. "|r" .. optionsText)
        
        yOffset = yOffset + 22
        
        -- Show items for this slot
        local allItems = {slot.primary}
        if slot.alternatives then
            for _, alt in ipairs(slot.alternatives) do
                table.insert(allItems, alt)
            end
        end
        
        local itemIndex = 0
        for _, item in ipairs(allItems) do
            itemIndex = itemIndex + 1
            
            local itemFrame = slotFrame.items[itemIndex]
            if not itemFrame then
                itemFrame = CreateFrame("Frame", nil, modal.scrollChild)
                itemFrame:SetSize(380, 22)
                slotFrame.items[itemIndex] = itemFrame
                
                -- Icon
                itemFrame.icon = itemFrame:CreateTexture(nil, "ARTWORK")
                itemFrame.icon:SetSize(20, 20)
                itemFrame.icon:SetPoint("LEFT", 25, 0)
                
                -- Name
                itemFrame.name = itemFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
                itemFrame.name:SetPoint("LEFT", itemFrame.icon, "RIGHT", 5, 0)
                itemFrame.name:SetWidth(240)
                itemFrame.name:SetJustifyH("LEFT")
                
                -- Status
                itemFrame.status = itemFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
                itemFrame.status:SetPoint("RIGHT", -10, 0)
            end
            
            itemFrame:SetPoint("TOPLEFT", 0, -yOffset)
            
            -- Set icon
            if item.icon then
                itemFrame.icon:SetTexture(item.icon)
            elseif item.itemID then
                local _, _, _, _, iconPath = GetItemInfoInstant(item.itemID)
                itemFrame.icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark")
            end
            
            -- Set name with quality color and collection status
            local nameColor
            if item.collected then
                nameColor = "|cff00ff00"  -- Green for collected
            elseif not slot.isComplete then
                nameColor = "|cffff8800"  -- Orange for NEEDED items
            else
                nameColor = "|cff888888"  -- Gray for not needed (slot already filled)
            end
            itemFrame.name:SetText(nameColor .. (item.name or "Unknown") .. "|r")
            
            -- Status text
            if item.collected then
                itemFrame.status:SetText("|cff00ff00✓ Collected|r")
            elseif not slot.isComplete then
                itemFrame.status:SetText("|cffff6600← Get this!|r")
            else
                itemFrame.status:SetText("|cff666666(not needed)|r")
            end
            
            itemFrame:Show()
            yOffset = yOffset + 22
        end
        
        -- Hide unused item frames
        for i = itemIndex + 1, #slotFrame.items do
            slotFrame.items[i]:Hide()
        end
        
        slotFrame:Show()
        yOffset = yOffset + 8  -- Gap between slots
    end
    
    -- Update scroll child height
    modal.scrollChild:SetHeight(yOffset + 20)
end

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

function SetsUI:Show(parent)
    if not self.frame then
        self:Create(parent)
    end
    
    self.frame:SetParent(parent)
    self.frame:SetAllPoints(parent)
    self.frame:Show()
    
    self:RefreshSetList()
end

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

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

-- Static popup at load time to prevent taint
StaticPopupDialogs["ETB_DELETE_CUSTOM_SET"] = {
    text = "Delete this custom set?",
    button1 = "Delete", button2 = "Cancel",
    OnAccept = function() SetsUI:DeleteCurrentSet() end,
    timeout = 0, whileDead = true, hideOnEscape = true, preferredIndex = 3,
}

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