-- =============================================================================
-- ETB v12.57.0 — UI/Windows/SetsUI/Detail.lua
-- =============================================================================
-- Third and final SetsUI submodule. Owns the RIGHT panel: detail view,
-- piece icons, set selection logic, and the three Display variants for
-- Blizzard / Custom / Community sets.
--
-- This is the largest cluster in SetsUI: 13 methods spanning ~1,200 lines.
-- After this extraction, SetsUI.lua becomes a thin orchestrator with
-- delegators, lifecycle, and the StaticPopup definition only.
--
-- Public API (all take owner = SetsUI singleton as first arg):
--   Detail:CreateSetDetailPanel(owner, parent)
--   Detail:CreatePieceIcons(owner, parent)
--   Detail:GetInventorySlotID(owner, slotName)
--   Detail:CycleSlotAlternative(owner, btn)
--   Detail:UpdatePieceIcons(owner, setID)            -- Blizzard variant
--   Detail:UpdateCommunitySetPieceIcons(owner, setID, communitySet)
--   Detail:UpdateCustomSetPieceIcons(owner, setID, customSet)
--   Detail:SelectSet(owner, setID)                   -- top-level dispatcher
--   Detail:DisplayCustomSet(owner, setID, detail)
--   Detail:DisplayCommunitySet(owner, setID, detail)
--   Detail:ConfirmDeleteSet(owner)
--   Detail:DeleteCurrentSet(owner)
--   Detail:PopulateVariantDropdown(owner, setID, setInfo)
--
-- Owner contract — fields read/written:
--   owner.setListFrame, owner.setDetailFrame
--   owner.pieceButtons
--   owner.selectedSetID, owner.currentDisplayedSetID
--
-- Owner methods called (delegators in main SetsUI.lua):
--   owner:PopulateSetList()       (List cluster)
--   owner:ShowSetSources()        (Sources cluster)
--   owner:SelectSet(setID)        (this cluster)
--   owner:DisplayCustomSet(...)
--   owner:DisplayCommunitySet(...)
--   owner:UpdatePieceIcons(setID)
--   owner:UpdateCustomSetPieceIcons(...)
--   owner:UpdateCommunitySetPieceIcons(...)
--   owner:PopulateVariantDropdown(...)
--   owner:CycleSlotAlternative(btn)
--   owner:GetInventorySlotID(slotName)
--   owner:ConfirmDeleteSet()
--   owner:DeleteCurrentSet()
--
-- Globals/Blizzard:
--   ETBDropdown (NS-stored; resolved at call time)
--   CreateFrame, GameTooltip
--   GetCursorPosition, GetItemInfo, GetItemInfoInstant
--   C_TransmogCollection (GetItemInfo, GetAppearanceSources, GetSourceInfo)
--   C_Timer.After
--   StaticPopup_Show
--   ETB.SetsData, ETB.CustomSets, ETB.CommunitySets
--   ETB:SafeSetTooltipItem, ETB:ResolveVisualID, ETB:ResolveVisualIDFromItemID
-- =============================================================================

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

NS.ETB.SetsUI = NS.ETB.SetsUI or ETB.SetsUI or {}
local Detail = {}
NS.ETB.SetsUI.Detail = Detail

-- =============================================================================
-- FILE-LOCAL CONSTANTS
-- =============================================================================
-- v12.57.0: moved verbatim from SetsUI.lua. Used only inside the Detail cluster.

-- 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",
}

-- =============================================================================
-- DETAIL PANEL CONSTRUCTION
-- =============================================================================

function Detail:CreateSetDetailPanel(owner, parent)
    local ETBDropdown = NS.ETBDropdown  -- Resolved at call time

    local panel = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    panel:SetPoint("TOPLEFT", owner.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 }
    })
    owner.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)
    Detail:CreatePieceIcons(owner, 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
    -- v15.8.8: bind OnUpdate only while dragging
    local function onUpdateRotate(self)
        local x = GetCursorPosition()
        self.rotation = (self.rotation or 0) + (x - self.startX) * 0.01
        self:SetRotation(self.rotation)
        self.startX = x
    end
    panel.model:SetScript("OnMouseDown", function(self, btn)
        if btn == "LeftButton" then
            self.startX = GetCursorPosition()
            self:SetScript("OnUpdate", onUpdateRotate)
        end
    end)
    panel.model:SetScript("OnMouseUp", function(self)
        self:SetScript("OnUpdate", nil)
    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()
        owner: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()
        owner:ConfirmDeleteSet()
    end)

    -- Send to Blizzard Custom Sets (native apply, incomplete-aware).
    -- The 5.5.4 client carries Blizzard's modern Wardrobe; rather than apply
    -- transmog ourselves, we hand the selected set to Blizzard's Custom Sets,
    -- which applies owned pieces and skips the rest.
    panel.sendToBlizzBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    panel.sendToBlizzBtn:SetSize(160, 24)
    panel.sendToBlizzBtn:SetPoint("BOTTOM", 0, 15)
    panel.sendToBlizzBtn:SetText("Send to Blizzard")
    panel.sendToBlizzBtn:SetScript("OnClick", function()
        if ETB.CustomSetExport then
            ETB.CustomSetExport:ExportBySelectedID(owner)
        end
    end)
    panel.sendToBlizzBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:AddLine("Send to Blizzard Custom Sets")
        GameTooltip:AddLine("Adds this set to Blizzard's native Custom Sets tab.", 0.8, 0.8, 0.8, true)
        GameTooltip:AddLine("Apply it there with the transmogrifier open — owned pieces apply, the rest are skipped.", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    panel.sendToBlizzBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    -- Only useful when the native custom-set API exists (5.5.4+).
    if not (ETB.CustomSetExport and ETB.CustomSetExport:IsAvailable()) then
        panel.sendToBlizzBtn:Hide()
    end

    parent.setDetailPanel = panel
end

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

function Detail:CreatePieceIcons(owner, parent)
    local iconSize = 55  -- Larger icons
    local spacing = 6
    local totalWidth = (#SLOT_ORDER * iconSize) + ((#SLOT_ORDER - 1) * spacing)
    local startX = -totalWidth / 2

    owner.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)
            owner: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_LEFT")
                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 = ETB.Style:Mark(item.collected)
                    -- Resolve the real name live — by hover time GetItemInfo is
                    -- almost always cached, so this replaces any "Loading…"/ID.
                    local rname = item.name
                    if (not rname or rname == "Loading..." or rname == tostring(item.itemID)) and item.itemID then
                        local live = GetItemInfo(item.itemID)
                        if live then rname = live; item.name = live; item.nameResolved = true end
                    end
                    rname = rname or ("Item " .. (item.itemID or "?"))
                    GameTooltip:AddLine(marker .. status .. " " .. rname, 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(ETB.Style.MARK_OWNED .. " |cff00ff00Slot Complete|r")
                else
                    GameTooltip:AddLine(ETB.Style.MARK_MISSING .. " |cffff4444SLOT 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 = owner.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 = owner: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)

        owner.pieceButtons[i] = btn
    end

    -- Also store in parent (detail frame) for easy access
    parent.pieceButtons = owner.pieceButtons
end

-- Helper to get inventory slot ID from our slot constant
function Detail:GetInventorySlotID(owner, 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 Detail:CycleSlotAlternative(owner, 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.sourceID = item.sourceID            -- v15.18.2: keep source ref in sync
        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 = owner.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()
            if btn.check then btn.check:Show() end
            if btn.missing then btn.missing:Hide() end
        else
            btn.icon:SetDesaturated(true)
            btn.desaturate:Show()
            if btn.check then btn.check:Hide() end
            if btn.missing then btn.missing:Show() end
        end

        -- v15.18.2 BUGFIX: shared-appearance items look identical on the
        -- model and often share an inventory icon, so the cycle previously
        -- gave the player no readable feedback — it looked like nothing
        -- changed. Surface WHICH alternative is now shown (position
        -- indicator) and re-show the item tooltip so the new item's name,
        -- source and obtainability update immediately, letting the player
        -- see which alternative is the easier one to obtain.
        if not btn.altCount then
            btn.altCount = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            btn.altCount:SetPoint("BOTTOMRIGHT", -2, 2)
        end
        btn.altCount:SetText(string.format("|cffffd100%d/%d|r",
            nextIndex, #btn.allItems))

        local onEnter = btn:GetScript("OnEnter")
        if onEnter then onEnter(btn) end
    end
end

-- =============================================================================
-- UPDATE PIECE ICONS — three variants for Blizzard / Custom / Community
-- =============================================================================

function Detail:UpdatePieceIcons(owner, setID)
    -- COMPREHENSIVE RESET - Reset ALL icons to clean state
    for i, btn in ipairs(owner.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 = owner.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. NOTE: read the per-item state
            -- from `self` at hover time, not captured locals — the cycle button
            -- updates self.itemID / self.currentItemCollected, and a closure that
            -- captured the primary would always show the wrong (original) item.
            local capturedSlotName = SLOT_NAMES[SLOT_ORDER[btnIdx]] or "Unknown"

            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
                local curItemID = self.itemID
                if curItemID and curItemID > 0 then
                    ETB:SafeSetTooltipItem(GameTooltip, curItemID)
                    GameTooltip:AddLine(" ")

                    -- Show slot status clearly
                    GameTooltip:AddLine("|cffffd100" .. capturedSlotName .. "|r")
                    if self.isCollected then
                        GameTooltip:AddLine(ETB.Style.MARK_OWNED .. " |cff00ff00Slot Complete|r")
                    else
                        GameTooltip:AddLine(ETB.Style.MARK_MISSING .. " |cffff4444SLOT NEEDED|r")
                    end

                    -- Show item collection status (for the CURRENT cycled item)
                    if self.currentItemCollected then
                        GameTooltip:AddLine("|cff00ff00This item: Collected|r")
                    else
                        GameTooltip:AddLine("|cffff6600This item: Not Collected|r")
                    end

                    -- Show alternatives info
                    local altCount = self.allItems and #self.allItems or 1
                    if altCount > 1 then
                        GameTooltip:AddLine(" ")
                        GameTooltip:AddLine(string.format("|cffffd100%d items can fill this slot|r", altCount))
                        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

function Detail:UpdateCommunitySetPieceIcons(owner, setID, communitySet)
    local pieceButtons = owner.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
                -- v15.18.2: align currentIndex with the item actually shown.
                -- allItems here is the appearance-source list in API order,
                -- not primary-first, so a blind index of 1 made the first
                -- cycle click mis-step.
                btn.currentIndex = 1
                for ai = 1, #alternatives do
                    if alternatives[ai].itemID == btn.itemID then
                        btn.currentIndex = ai
                        break
                    end
                end
                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). Read per-item state from
            -- `self` at hover time so cycling shows the correct item.
            local capturedSlotName = slotDisplayName

            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
                local curItemID = self.itemID
                if curItemID and curItemID > 0 then ETB:SafeSetTooltipItem(GameTooltip, curItemID) else GameTooltip:SetText(capturedSlotName or "Unknown Item") end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("|cffffd100" .. capturedSlotName .. "|r")

                if self.isCollected then
                    GameTooltip:AddLine(ETB.Style.MARK_OWNED .. " |cff00ff00Slot Complete|r")
                else
                    GameTooltip:AddLine(ETB.Style.MARK_MISSING .. " |cffff4444SLOT NEEDED|r")
                end

                if self.currentItemCollected then
                    GameTooltip:AddLine("|cff00ff00This item: Collected|r")
                else
                    GameTooltip:AddLine("|cffff6600This item: Not Collected|r")
                end

                local altCount = self.allItems and #self.allItems or 0
                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

function Detail:UpdateCustomSetPieceIcons(owner, setID, customSet)
    local pieceButtons = owner.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
                -- v15.18.2: align currentIndex with the item actually shown
                -- (allItems is the appearance-source list in API order).
                btn.currentIndex = 1
                for ai = 1, #alternatives do
                    if alternatives[ai].itemID == btn.itemID then
                        btn.currentIndex = ai
                        break
                    end
                end
                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. Read per-item state from
            -- `self` at hover time so cycling shows the correct item.
            local slotName = SLOT_NAMES_CUSTOM[slotIndex] or "Unknown Slot"

            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
                local curItemID = self.itemID
                if curItemID and curItemID > 0 then ETB:SafeSetTooltipItem(GameTooltip, curItemID) else GameTooltip:SetText(slotName or "Unknown Item") end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("|cffffd100" .. slotName .. "|r")

                if self.isCollected then
                    GameTooltip:AddLine(ETB.Style.MARK_OWNED .. " |cff00ff00Slot Complete|r")
                else
                    GameTooltip:AddLine(ETB.Style.MARK_MISSING .. " |cffff4444SLOT NEEDED|r")
                end

                if self.currentItemCollected then
                    GameTooltip:AddLine("|cff00ff00This item: Collected|r")
                else
                    GameTooltip:AddLine("|cffff6600This item: Not Collected|r")
                end

                local altCount = self.allItems and #self.allItems or 0
                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

-- =============================================================================
-- SET SELECTION DISPATCHER
-- =============================================================================

function Detail:SelectSet(owner, setID)
    owner.selectedSetID = setID

    -- Refresh list to update highlighting
    owner:PopulateSetList()

    local detail = owner.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
        owner:DisplayCommunitySet(setID, detail)
        return
    end

    if isCustomSet and ETB.CustomSets then
        -- Handle CUSTOM SET
        owner: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
    owner.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(ETB.Style.MARK_OWNED .. " |cff00ff00COMPLETE! (" .. 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
    owner:PopulateVariantDropdown(setID, setInfo)

    -- Update piece icons
    owner: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

-- =============================================================================
-- DISPLAY VARIANTS — Custom and Community sets
-- =============================================================================

function Detail:DisplayCustomSet(owner, setID, detail)
    local ETBDropdown = NS.ETBDropdown  -- Resolved at call time

    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
    owner.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(ETB.Style.MARK_OWNED .. " |cff00ff00COMPLETE! (" .. 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
    owner: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

function Detail:DisplayCommunitySet(owner, setID, detail)
    local ETBDropdown = NS.ETBDropdown  -- Resolved at call time

    local communitySet = ETB.CommunitySets:GetSet(setID)
    if not communitySet then
        detail.setName:SetText("|cffff0000Community set not found|r")
        return
    end

    owner.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(ETB.Style.MARK_OWNED .. " |cff00ff00COMPLETE! (" .. 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)
    owner: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 owner.currentDisplayedSetID == capturedSetID then
                owner:UpdateCommunitySetPieceIcons(capturedSetID, capturedSet)
            end
        end)
        -- Second retry for slow network
        C_Timer.After(2.0, function()
            if owner.currentDisplayedSetID == capturedSetID then
                owner:UpdateCommunitySetPieceIcons(capturedSetID, capturedSet)
            end
        end)
    end
end

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

function Detail:ConfirmDeleteSet(owner)
    if not owner.currentDisplayedSetID then
        ETB:Print("|cffff0000No set selected|r")
        return
    end

    local setID = owner.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 in main file)
    StaticPopup_Show("ETB_DELETE_CUSTOM_SET")
end

function Detail:DeleteCurrentSet(owner)
    if not owner.currentDisplayedSetID then return end

    local setID = owner.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
    owner.currentDisplayedSetID = nil
    owner.selectedSetID = nil

    -- Refresh the list
    owner:PopulateSetList()

    -- Reset detail panel
    local detail = owner.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

-- =============================================================================
-- VARIANT DROPDOWN
-- =============================================================================

function Detail:PopulateVariantDropdown(owner, setID, setInfo)
    local ETBDropdown = NS.ETBDropdown  -- Resolved at call time

    local detail = owner.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()
                    owner:SelectSet(setData.setID)
                end
                ETBDropdown:AddButton(_items_, info)
            end
        end
    end)
end
