--[[
============================================================================
ETB - AppearanceDetailPanel.lua
============================================================================
Blizzard-style appearance detail view:
  Left:  3D model preview + item name + collection status
  Right: "Items That Share This Appearance" list with source info

Opens on left-click in grouped appearance mode (replaces old cycle behavior).
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB
if not ETB then return end

ETB.AppearanceDetail = ETB.AppearanceDetail or {}
local ADP = ETB.AppearanceDetail

ADP.frame = nil
ADP.currentItem = nil
ADP.rotation = 0

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

local PANEL_WIDTH = 680
local PANEL_HEIGHT = 520
local MODEL_WIDTH = 280
local LIST_WIDTH = 380
local ROW_HEIGHT = 75
local MAX_ROWS = 6

local QUALITY_COLORS = {
    [0] = "|cff9d9d9d",  -- Poor
    [1] = "|cffffffff",  -- Common
    [2] = "|cff1eff00",  -- Uncommon
    [3] = "|cff0070dd",  -- Rare
    [4] = "|cffa335ee",  -- Epic
    [5] = "|cffff8000",  -- Legendary
}

-- ============================================================================
-- RESOLVE VISUAL ID
-- Uses the shared ETB:ResolveVisualID() from Init.lua
-- which caches and resolves via C_TransmogCollection.GetItemInfo()
-- ============================================================================

local function ResolveVisualID(item)
    return ETB:ResolveVisualID(item)
end

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

function ADP:Create()
    if self.frame then return self.frame end

    -- Get the main frame to use as parent
    local mainFrame = (ETB.MainFrame and ETB.MainFrame.frame) or UIParent

    -- Semi-transparent overlay behind the panel to dim the grid
    local overlay = CreateFrame("Frame", "ETBAppearanceDetailOverlay", mainFrame)
    overlay:SetAllPoints()
    overlay:SetFrameStrata("FULLSCREEN_DIALOG")
    overlay:SetFrameLevel(200)
    overlay:EnableMouse(true)  -- Block clicks to grid behind
    overlay.bg = overlay:CreateTexture(nil, "BACKGROUND")
    overlay.bg:SetAllPoints()
    overlay.bg:SetColorTexture(0, 0, 0, 0.5)
    overlay:SetScript("OnMouseDown", function()
        ADP:Hide()
    end)
    overlay:Hide()
    self.overlay = overlay

    local f = CreateFrame("Frame", "ETBAppearanceDetailPanel", overlay, "BackdropTemplate")
    f:SetSize(PANEL_WIDTH, PANEL_HEIGHT)
    f:SetPoint("CENTER", mainFrame, "CENTER", 0, 0)
    f:SetFrameStrata("FULLSCREEN_DIALOG")
    f:SetFrameLevel(210)
    f:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left = 8, right = 8, top = 8, bottom = 8 },
    })
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        -- Clamp within parent so it can't be dragged off
        self:SetClampedToScreen(true)
    end)
    f:SetClampedToScreen(true)

    -- Close button (X)
    local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -2, -2)
    closeBtn:SetScript("OnClick", function() ADP:Hide() end)

    -- ESC closes panel (without closing MainFrame).
    -- v14.10.1: see Construction.lua for rationale. Note: there used to be
    -- TWO OnKeyDown handlers on this same frame (this one + another below
    -- in CreateListSide), with the second overwriting the first. Both have
    -- now been replaced; this single UISpecialFrames registration is enough.
    if UISpecialFrames then
        local already = false
        for _, name in ipairs(UISpecialFrames) do
            if name == "ETBAppearanceDetailPanel" then already = true; break end
        end
        if not already then tinsert(UISpecialFrames, "ETBAppearanceDetailPanel") end
    end

    -- ========================================================================
    -- LEFT SIDE: Model Preview
    -- ========================================================================

    local modelFrame = CreateFrame("Frame", nil, f, "BackdropTemplate")
    modelFrame:SetSize(MODEL_WIDTH, PANEL_HEIGHT - 30)
    modelFrame:SetPoint("TOPLEFT", 12, -12)
    modelFrame:SetBackdrop({
        bgFile = "Interface\\Collections\\CollectionsBackgroundTile",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 256, edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    f.modelFrame = modelFrame

    -- Item name at top
    f.itemName = modelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    f.itemName:SetPoint("TOP", modelFrame, "TOP", 0, -12)
    f.itemName:SetWidth(MODEL_WIDTH - 20)
    f.itemName:SetText("")

    -- Slot label
    f.slotLabel = modelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.slotLabel:SetPoint("TOP", f.itemName, "BOTTOM", 0, -2)
    f.slotLabel:SetTextColor(0.6, 0.6, 0.6)

    -- 3D Model (250px height leaves room for rich source data below)
    f.model = CreateFrame("DressUpModel", nil, modelFrame)
    f.model:SetSize(MODEL_WIDTH - 10, 250)
    f.model:SetPoint("TOP", f.slotLabel, "BOTTOM", 0, -4)
    f.model:SetUnit("player")
    f.model:SetRotation(0)
    f.model:SetPosition(0, 0, 0)

    -- Drag to rotate
    f.model:EnableMouse(true)
    -- v15.8.8: bind OnUpdate only while dragging
    local function onUpdateRotate(self)
        local x = GetCursorPosition()
        local diff = (x - self._startX) * 0.01
        ADP.rotation = ADP.rotation + diff
        self:SetRotation(ADP.rotation)
        self._startX = x
    end
    f.model:SetScript("OnMouseDown", function(self, button)
        if button == "LeftButton" then
            self._startX = GetCursorPosition()
            self:SetScript("OnUpdate", onUpdateRotate)
        end
    end)
    f.model:SetScript("OnMouseUp", function(self)
        self:SetScript("OnUpdate", nil)
    end)

    -- Mouse wheel zoom
    f.model:EnableMouseWheel(true)
    f.model:SetScript("OnMouseWheel", function(self, delta)
        local x, y, z = self:GetPosition()
        z = z + (delta > 0 and 0.15 or -0.15)
        self:SetPosition(x, y, math.max(-1, math.min(2, z)))
    end)

    -- Collection status (anchored below model)
    f.collectionBadge = modelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    f.collectionBadge:SetPoint("TOP", f.model, "BOTTOM", 0, -6)

    -- Source info block (multi-line, shows full source data)
    f.sourceInfo = modelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.sourceInfo:SetPoint("TOP", f.collectionBadge, "BOTTOM", 0, -4)
    f.sourceInfo:SetWidth(MODEL_WIDTH - 20)
    f.sourceInfo:SetSpacing(2)
    f.sourceInfo:SetJustifyH("CENTER")

    -- Extended source details (location, difficulty, expansion, etc.)
    f.sourceDetail = modelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.sourceDetail:SetPoint("TOP", f.sourceInfo, "BOTTOM", 0, -2)
    f.sourceDetail:SetWidth(MODEL_WIDTH - 20)
    f.sourceDetail:SetSpacing(2)
    f.sourceDetail:SetJustifyH("CENTER")

    -- "Collected via" note
    f.collectionNote = modelFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.collectionNote:SetPoint("TOP", f.sourceDetail, "BOTTOM", 0, -2)
    f.collectionNote:SetWidth(MODEL_WIDTH - 20)
    f.collectionNote:SetTextColor(0.2, 0.8, 0.2)

    -- ========================================================================
    -- RIGHT SIDE: Items That Share This Appearance
    -- ========================================================================

    local listFrame = CreateFrame("Frame", nil, f, "BackdropTemplate")
    listFrame:SetSize(LIST_WIDTH, PANEL_HEIGHT - 30)
    listFrame:SetPoint("TOPLEFT", modelFrame, "TOPRIGHT", 4, 0)
    listFrame:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 32, edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    f.listFrame = listFrame

    -- Header
    f.listHeader = listFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    f.listHeader:SetPoint("TOP", listFrame, "TOP", 0, -12)
    f.listHeader:SetText("|cffffd100Items That Share This Appearance|r")
    f.listHeader:SetWidth(LIST_WIDTH - 16)

    -- Separator
    local sep = listFrame:CreateTexture(nil, "ARTWORK")
    sep:SetSize(LIST_WIDTH - 20, 1)
    sep:SetPoint("TOP", f.listHeader, "BOTTOM", 0, -6)
    sep:SetColorTexture(0.4, 0.4, 0.4, 0.6)

    -- Scroll frame for item rows
    local scrollFrame = CreateFrame("ScrollFrame", nil, listFrame, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", sep, "BOTTOMLEFT", 2, -4)
    scrollFrame:SetPoint("BOTTOMRIGHT", listFrame, "BOTTOMRIGHT", -24, 8)
    f.scrollFrame = scrollFrame

    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetSize(LIST_WIDTH - 40, 1)  -- Height set dynamically
    scrollFrame:SetScrollChild(scrollChild)
    f.scrollChild = scrollChild

    -- Pre-create item rows
    f.itemRows = {}
    for i = 1, 20 do
        f.itemRows[i] = self:CreateItemRow(scrollChild, i)
    end

    -- ESC closes panel: handled at top of Create() via UISpecialFrames.
    -- v14.10.1: removed redundant OnKeyDown handler that was overwriting the
    -- first one anyway.

    f:Hide()
    self.frame = f
    return f
end

-- ============================================================================
-- ITEM ROW (like Blizzard's shared appearance list)
-- ============================================================================

function ADP:CreateItemRow(parent, index)
    local row = CreateFrame("Button", nil, parent)
    row:SetSize(LIST_WIDTH - 44, ROW_HEIGHT)
    row:SetPoint("TOPLEFT", 0, -((index - 1) * (ROW_HEIGHT + 2)))

    -- Highlight
    row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")

    -- Background (alternating)
    row.bg = row:CreateTexture(nil, "BACKGROUND")
    row.bg:SetAllPoints()
    row.bg:SetColorTexture(0.12, 0.12, 0.12, (index % 2 == 0) and 0.6 or 0.3)

    -- Selected highlight (for current item)
    row.selectedBg = row:CreateTexture(nil, "BACKGROUND", nil, 1)
    row.selectedBg:SetAllPoints()
    row.selectedBg:SetColorTexture(0.2, 0.35, 0.2, 0.5)
    row.selectedBg:Hide()

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

    -- Icon border
    row.iconBorder = row:CreateTexture(nil, "OVERLAY")
    row.iconBorder:SetSize(44, 44)
    row.iconBorder:SetPoint("CENTER", row.icon, "CENTER")
    row.iconBorder:SetTexture("Interface\\Common\\WhiteIconFrame")

    -- Item name (line 1, top-left)
    row.name = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.name:SetPoint("TOPLEFT", row.icon, "TOPRIGHT", 8, -1)
    row.name:SetWidth(LIST_WIDTH - 160)
    row.name:SetJustifyH("LEFT")
    row.name:SetWordWrap(false)

    -- Collection status (top-right: "Collected" / "Locked")
    row.status = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.status:SetPoint("TOPRIGHT", -8, -3)

    -- Collection detail line (line 2: "Collected, but Not From This Item")
    row.collectedVia = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.collectedVia:SetPoint("TOPLEFT", row.name, "BOTTOMLEFT", 0, -1)
    row.collectedVia:SetWidth(LIST_WIDTH - 100)
    row.collectedVia:SetJustifyH("LEFT")
    row.collectedVia:SetTextColor(0.6, 0.6, 0.6)

    -- "via: ItemName" line (line 3: shows which item provided the appearance)
    row.viaLine = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.viaLine:SetPoint("TOPLEFT", row.collectedVia, "BOTTOMLEFT", 0, -1)
    row.viaLine:SetWidth(LIST_WIDTH - 100)
    row.viaLine:SetJustifyH("LEFT")
    row.viaLine:SetTextColor(0.7, 0.7, 0.7)

    -- Source info line (line 4: "◆ Crafted | Tailoring" or "Drops from Boss, Instance")
    row.source = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.source:SetPoint("TOPLEFT", row.viaLine, "BOTTOMLEFT", 4, -1)
    row.source:SetWidth(LIST_WIDTH - 110)
    row.source:SetJustifyH("LEFT")
    row.source:SetTextColor(0.5, 0.5, 0.5)

    -- Click handlers
    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    row:SetScript("OnClick", function(self, button)
        if button == "LeftButton" then
            if self.itemID then
                -- Preview this specific item
                ADP:PreviewSpecificItem(self.itemID)
            end
        elseif button == "RightButton" then
            if self.itemID then
                local _, link = GetItemInfo(self.itemID)
                if link then
                    GameTooltip:SetOwner(self, "ANCHOR_LEFT")
                    pcall(GameTooltip.SetHyperlink, GameTooltip, link)
                    GameTooltip:Show()
                end
            end
        end
    end)

    row:SetScript("OnEnter", function(self)
        if self.itemID then
            GameTooltip:SetOwner(self, "ANCHOR_LEFT")
            local _, link = GetItemInfo(self.itemID)
            if link then
                pcall(GameTooltip.SetHyperlink, GameTooltip, link)
            else
                GameTooltip:SetText("Item " .. self.itemID)
                GetItemInfo(self.itemID)
            end
            GameTooltip:Show()
        end
    end)
    row:SetScript("OnLeave", function() GameTooltip:Hide() end)

    row:Hide()
    return row
end

-- ============================================================================
-- SHOW APPEARANCE
-- ============================================================================

function ADP:Show(item)
    if not item then return end
    if not self.frame then self:Create() end

    self.currentItem = item
    self.rotation = 0

    -- Reparent overlay to whichever parent is visible
    local tuiFrame = ETB.TransmogUI and ETB.TransmogUI.frame
    local mainFrame = (ETB.MainFrame and ETB.MainFrame.frame and ETB.MainFrame.frame:IsVisible() and ETB.MainFrame.frame)
        or (tuiFrame and tuiFrame:IsVisible() and tuiFrame)
        or UIParent

    if self.overlay then
        self.overlay:SetParent(mainFrame)
        self.overlay:SetAllPoints()
        self.overlay:Show()
    end

    -- Center on the parent frame
    self.frame:ClearAllPoints()
    self.frame:SetPoint("CENTER", mainFrame, "CENTER", 0, 0)

    -- Item name with quality color
    local color = QUALITY_COLORS[item.quality or 1] or "|cffffffff"
    self.frame.itemName:SetText(color .. (item.name or "Unknown") .. "|r")

    -- Slot label (like the appearance tab: "Back", "Shoulder", etc.)
    local slotName = ""
    -- Try to get slot from invType/equipSlot
    local INVTYPE_NAMES = {
        INVTYPE_HEAD = "Head", INVTYPE_SHOULDER = "Shoulder",
        INVTYPE_CHEST = "Chest", INVTYPE_ROBE = "Chest",
        INVTYPE_WAIST = "Waist", INVTYPE_LEGS = "Legs",
        INVTYPE_FEET = "Feet", INVTYPE_WRIST = "Wrist",
        INVTYPE_HAND = "Hands", INVTYPE_CLOAK = "Back",
        INVTYPE_BODY = "Shirt", INVTYPE_TABARD = "Tabard",
        INVTYPE_WEAPON = "One-Hand", INVTYPE_WEAPONMAINHAND = "Main Hand",
        INVTYPE_WEAPONOFFHAND = "Off Hand", INVTYPE_2HWEAPON = "Two-Hand",
        INVTYPE_SHIELD = "Off Hand", INVTYPE_HOLDABLE = "Off Hand",
        INVTYPE_RANGED = "Ranged", INVTYPE_RANGEDRIGHT = "Ranged",
    }
    if item.invType and INVTYPE_NAMES[item.invType] then
        slotName = INVTYPE_NAMES[item.invType]
    elseif item.equipSlot and INVTYPE_NAMES[item.equipSlot] then
        slotName = INVTYPE_NAMES[item.equipSlot]
    elseif item.categoryName then
        slotName = item.categoryName
    elseif item.armorType then
        slotName = item.armorType
    end
    -- Also try runtime GetItemInfoInstant for slot detection
    if slotName == "" and item.itemID then
        local _, _, _, invType = GetItemInfoInstant(item.itemID)
        if invType and INVTYPE_NAMES[invType] then
            slotName = INVTYPE_NAMES[invType]
        end
    end
    self.frame.slotLabel:SetText(slotName)

    -- Model
    self.frame.model:SetUnit("player")
    self.frame.model:Undress()
    self.frame.model:SetRotation(0)
    self.frame.model:SetPosition(0, 0, 0)

    if item.itemID then
        C_Timer.After(0.1, function()
            if self.currentItem == item then
                local _, link = GetItemInfo(item.itemID)
                local tryOnArg = link or ("item:" .. item.itemID)
                pcall(self.frame.model.TryOn, self.frame.model, tryOnArg)
            end
        end)
    end

    -- Collection status
    if item.isCollected then
        self.frame.collectionBadge:SetText("|cff00ff00\226\156\147 Collected|r")
    else
        self.frame.collectionBadge:SetText("|cffff6666\226\156\151 Not Collected|r")
    end

    -- Source info (full source data — mirrors tooltip)
    local sourceText = ""
    local detailText = ""
    local collectionNoteText = ""
    
    -- Try FormatSourceTooltip first (richest data, merges all sources)
    if item.itemID and ETB.SourceMapper and ETB.SourceMapper.FormatSourceTooltip then
        local lines = ETB.SourceMapper:FormatSourceTooltip(item.itemID)
        if lines and #lines > 0 then
            -- First line is source type (e.g. "|cffffff00Boss Drop|r")
            sourceText = lines[1]
            -- Remaining lines are details (From:, Location:, Difficulty:, etc.)
            local details = {}
            for i = 2, #lines do
                details[#details + 1] = lines[i]
            end
            detailText = table.concat(details, "\n")
        end
    end
    
    -- Fallback: build from SourceData_ATT directly
    if sourceText == "" and item.itemID and ETB.SourceData_ATT and ETB.SourceData_ATT.GetPrimarySource then
        local src = ETB.SourceData_ATT:GetPrimarySource(item.itemID)
        if src then
            -- Source type with color
            local SOURCE_TYPE_COLORS = {
                BOSS = "|cffff4444", VENDOR = "|cff00ccff", QUEST = "|cffffff00",
                CRAFTED = "|cffff7c0a", PROFESSION = "|cffff7c0a", PVP = "|cff00ff00",
                WORLD_DROP = "|cff9d9d9d", ACHIEVEMENT = "|cffffff00", REPUTATION = "|cff00ccff",
                RARE_MOB = "|cff00ffff",
            }
            local typeColor = SOURCE_TYPE_COLORS[src.type] or "|cff888888"
            local typeName = src.type or "Unknown"
            if src.isRaid then typeName = "Raid Boss"
            elseif src.type == "BOSS" then typeName = "Dungeon Boss"
            end
            sourceText = typeColor .. typeName .. "|r"
            
            local details = {}
            if src.name and src.name ~= "Unknown" then
                details[#details + 1] = "|cffffd100From:|r " .. src.name
            end
            if src.location and src.location ~= "" then
                details[#details + 1] = "|cffffd100Location:|r " .. src.location
            end
            if src.difficulty then
                details[#details + 1] = "|cffffd100Difficulty:|r " .. src.difficulty
            end
            if src.expansion then
                details[#details + 1] = "|cffffd100Expansion:|r " .. src.expansion
            end
            if src.coordinates and ETB.SourceData_ATT.FormatCoords then
                local coordStr = ETB.SourceData_ATT:FormatCoords(src.coordinates)
                if coordStr then
                    details[#details + 1] = "|cffffd100Coords:|r " .. coordStr
                end
            end
            if src.classRestrictions and ETB.SourceData_ATT.FormatClasses then
                local classStr = ETB.SourceData_ATT:FormatClasses(src.classRestrictions)
                if classStr then
                    details[#details + 1] = "|cffffd100Classes:|r " .. classStr
                end
            end
            detailText = table.concat(details, "\n")
        end
    end
    
    -- Second fallback: item's dropSource field
    if sourceText == "" and item.dropSource then
        sourceText = "|cff888888" .. item.dropSource .. "|r"
    end
    if sourceText == "" and item.sourceType then
        sourceText = "|cff888888" .. item.sourceType .. "|r"
    end
    
    self.frame.sourceInfo:SetText(sourceText)
    self.frame.sourceDetail:SetText(detailText)
    
    -- Collection note (e.g. "Collected via: Icebound Cloak")
    local resolvedVisualID = ResolveVisualID(item)
    if item.isCollected and resolvedVisualID and C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
        local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, resolvedVisualID)
        if ok and sources then
            for _, src in ipairs(sources) do
                local siOk, si = pcall(C_TransmogCollection.GetSourceInfo, src.sourceID)
                if siOk and si and si.isCollected then
                    local cName = GetItemInfo(src.itemID)
                    if cName and src.itemID ~= item.itemID then
                        collectionNoteText = "|cff1eff00Collected via: " .. cName .. "|r"
                    end
                    break
                end
            end
        end
    end
    self.frame.collectionNote:SetText(collectionNoteText)

    -- Build shared items list
    self:PopulateSharedItems(item)

    self.frame:Show()
end

-- ============================================================================
-- POPULATE SHARED ITEMS LIST
-- ============================================================================

function ADP:PopulateSharedItems(item)
    -- Hide all rows first
    for _, row in ipairs(self.frame.itemRows) do
        row:Hide()
    end

    if not item then return end

    -- Get all items sharing this visual appearance
    local sharedItems = {}

    -- Resolve visualID via runtime API if not already set
    local resolvedVisualID = ResolveVisualID(item)

    -- Method 1: Use alternatives from grouped data
    if item.alternatives and type(item.alternatives) == "table" and #item.alternatives > 0 then
        for _, alt in ipairs(item.alternatives) do
            sharedItems[#sharedItems + 1] = alt
        end
    end

    -- Method 2: Query API for appearance sources using resolved visualID
    if #sharedItems <= 1 and resolvedVisualID and C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
        local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, resolvedVisualID)
        if ok and sources then
            local seen = {}
            for _, src in ipairs(sources) do
                if src.itemID and not seen[src.itemID] then
                    seen[src.itemID] = true
                    local siOk, srcInfo = pcall(C_TransmogCollection.GetSourceInfo, src.sourceID)
                    if not siOk then srcInfo = nil end
                    local name, _, quality, _, _, _, _, _, _, icon = GetItemInfo(src.itemID)
                    sharedItems[#sharedItems + 1] = {
                        itemID = src.itemID,
                        sourceID = src.sourceID,
                        name = name or ("Item " .. src.itemID),
                        quality = quality or (srcInfo and srcInfo.quality) or 1,
                        icon = icon,
                        isCollected = src.isCollected,
                        visualID = resolvedVisualID,
                    }
                    -- Request name if not cached
                    if not name then GetItemInfo(src.itemID) end
                end
            end
        end
    end

    -- Fallback: just show the current item
    if #sharedItems == 0 then
        sharedItems[1] = item
    end

    -- Find which specific sourceID is the one the player collected (for "via:" display)
    local collectedSourceItemName = nil
    if resolvedVisualID and C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
        local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, resolvedVisualID)
        if ok and sources then
            for _, src in ipairs(sources) do
                local siOk, si = pcall(C_TransmogCollection.GetSourceInfo, src.sourceID)
                if siOk and si and si.isCollected then
                    local cName = GetItemInfo(src.itemID)
                    if cName then
                        collectedSourceItemName = cName
                        break
                    end
                end
            end
        end
    end

    -- Sort: collected first, then by quality desc
    table.sort(sharedItems, function(a, b)
        if a.isCollected ~= b.isCollected then
            return a.isCollected and true or false
        end
        return (tonumber(a.quality) or 0) > (tonumber(b.quality) or 0)
    end)

    -- Source type colors for the diamond icon
    local SOURCE_TYPE_COLORS = {
        [1] = "|cffff8000",  -- Boss: orange
        [2] = "|cffffff00",  -- Quest: yellow
        [3] = "|cffffffff",  -- Vendor: white
        [4] = "|cff1eff00",  -- World Drop: green
        [5] = "|cffffff00",  -- Achievement: yellow
        [6] = "|cffff7c0a",  -- Crafted: druid orange
        [7] = "|cff9d9d9d",  -- Unavailable: gray
    }
    local SOURCE_TYPE_NAMES = {
        [1] = "Boss Drop",
        [2] = "Quest",
        [3] = "Vendor",
        [4] = "World Drop",
        [5] = "Achievement",
        [6] = "Crafted",
        [7] = "Unavailable",
    }

    -- Populate rows
    local totalHeight = 0
    for i, shared in ipairs(sharedItems) do
        if i > #self.frame.itemRows then break end

        local row = self.frame.itemRows[i]
        row.itemID = shared.itemID

        -- Icon
        local icon = shared.icon
        if not icon and shared.itemID then
            local _, _, _, _, iconPath = GetItemInfoInstant(shared.itemID)
            icon = iconPath
        end
        row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")

        -- Icon border color by quality
        local r, g, b = 1, 1, 1
        if shared.quality and type(shared.quality) == "number" then
            local ok, qr, qg, qb = pcall(GetItemQualityColor, shared.quality)
            if ok and qr then r, g, b = qr, qg, qb end
        end
        row.iconBorder:SetVertexColor(r, g, b)

        -- Name
        local nameColor = QUALITY_COLORS[shared.quality or 1] or "|cffffffff"
        local displayName = shared.name or "Loading..."
        if displayName == "Loading..." and shared.itemID then
            local n = GetItemInfo(shared.itemID)
            if n then displayName = n; shared.name = n end
        end
        row.name:SetText(nameColor .. displayName .. "|r")

        -- Collection status badge (top-right)
        if shared.isCollected then
            row.status:SetText("|cff00ff00Collected|r")
        else
            row.status:SetText("|cff888888Locked|r")
        end

        -- Highlight the currently selected/primary item
        if shared.itemID == item.itemID then
            row.selectedBg:Show()
        else
            row.selectedBg:Hide()
        end

        -- Line 2: Collection detail
        local collectedText = ""
        local isThisSourceCollected = false
        if shared.isCollected then
            if shared.sourceID and C_TransmogCollection then
                local siOk, si = pcall(C_TransmogCollection.GetSourceInfo, shared.sourceID)
                if siOk and si and si.isCollected then
                    isThisSourceCollected = true
                    collectedText = "Collected, |cff00ff00From This Item|r"
                else
                    collectedText = "Collected, but |cffffd100Not From This Item|r"
                end
            else
                collectedText = "Collected"
            end
        else
            collectedText = "|cff888888Unknown|r"
        end
        row.collectedVia:SetText(collectedText)

        -- Line 3: "via: ItemName" (shows which item provided this appearance)
        local viaText = ""
        if shared.isCollected and not isThisSourceCollected and collectedSourceItemName then
            viaText = "|cffffd100via:|r " .. collectedSourceItemName
        elseif isThisSourceCollected then
            viaText = "|cffffd100via:|r " .. displayName
        end
        row.viaLine:SetText(viaText)

        -- Line 4: Source info with type icon
        local srcLine = ""
        if shared.itemID then
            -- Try SourceMapper first for rich data
            if ETB.SourceMapper and ETB.SourceMapper.initialized then
                local src = ETB.SourceMapper:GetPrimarySource(shared.itemID)
                if src then
                    local sourceType = src.sourceType
                    local typeColor = SOURCE_TYPE_COLORS[sourceType] or "|cff888888"
                    local typeName = SOURCE_TYPE_NAMES[sourceType] or "Unknown"

                    if sourceType == 1 then  -- Boss Drop
                        local parts = {}
                        if src.sourceName and src.sourceName ~= "Unknown" and src.sourceName ~= "Boss Drop" then
                            parts[#parts + 1] = src.sourceName
                        end
                        if src.sourceLocation and src.sourceLocation ~= "" then
                            parts[#parts + 1] = src.sourceLocation
                        end
                        if #parts > 0 then
                            srcLine = typeColor .. "Drops from " .. table.concat(parts, ", ") .. "|r"
                        else
                            srcLine = typeColor .. "\226\151\134 Boss Drop|r"
                        end
                    elseif sourceType == 6 then  -- Crafted
                        local prof = src.sourceName or "Crafting"
                        srcLine = typeColor .. "\226\151\134 Crafted | " .. prof .. "|r"
                    elseif sourceType == 2 then  -- Quest
                        local loc = src.sourceLocation or ""
                        if loc ~= "" then
                            srcLine = typeColor .. "\226\151\134 Quest | " .. loc .. "|r"
                        else
                            srcLine = typeColor .. "\226\151\134 Quest Reward|r"
                        end
                    elseif sourceType == 3 then  -- Vendor
                        local vendor = src.sourceName or "Vendor"
                        local loc = src.sourceLocation or ""
                        if loc ~= "" then
                            srcLine = typeColor .. "\226\151\134 " .. vendor .. " | " .. loc .. "|r"
                        else
                            srcLine = typeColor .. "\226\151\134 Vendor|r"
                        end
                    elseif sourceType == 4 then  -- World Drop
                        srcLine = typeColor .. "\226\151\134 World Drop|r"
                    else
                        local parts = {}
                        if src.sourceName and src.sourceName ~= "Unknown" then
                            parts[#parts + 1] = src.sourceName
                        end
                        if src.sourceLocation and src.sourceLocation ~= "" then
                            parts[#parts + 1] = src.sourceLocation
                        end
                        if #parts > 0 then
                            srcLine = typeColor .. "\226\151\134 " .. typeName .. " | " .. table.concat(parts, ", ") .. "|r"
                        else
                            srcLine = typeColor .. "\226\151\134 " .. typeName .. "|r"
                        end
                    end
                end
            end

            -- Fallback to compact data
            if srcLine == "" and ETB.SourceData_ATT then
                local attSrc = ETB.SourceData_ATT:GetPrimarySource(shared.itemID)
                if attSrc and attSrc.name and attSrc.name ~= "Unknown" then
                    local typeColor = "|cff888888"
                    if attSrc.type == "BOSS" then typeColor = "|cffff8000"
                    elseif attSrc.type == "CRAFTED" then typeColor = "|cffff7c0a"
                    elseif attSrc.type == "QUEST" then typeColor = "|cffffff00"
                    elseif attSrc.type == "VENDOR" then typeColor = "|cffffffff"
                    elseif attSrc.type == "WORLD_DROP" then typeColor = "|cff1eff00"
                    end

                    local parts = {}
                    if attSrc.name then parts[#parts + 1] = attSrc.name end
                    if attSrc.location and attSrc.location ~= "" then parts[#parts + 1] = attSrc.location end
                    srcLine = typeColor .. "\226\151\134 " .. table.concat(parts, ", ") .. "|r"
                end
            end
        end
        
        -- Add class restriction tag if applicable
        if shared.itemID and ETB.ClassRestrictions then
            local restriction = ETB.ClassRestrictions:FormatRestriction(shared.itemID)
            if restriction then
                if srcLine ~= "" then
                    srcLine = srcLine .. "  " .. restriction
                else
                    srcLine = restriction
                end
            end
        end
        
        row.source:SetText(srcLine)

        row:Show()
        totalHeight = totalHeight + ROW_HEIGHT + 2
    end

    self.frame.scrollChild:SetHeight(math.max(totalHeight, 1))
end

-- ============================================================================
-- PREVIEW SPECIFIC ITEM (clicked from list)
-- ============================================================================

function ADP:PreviewSpecificItem(itemID)
    if not itemID or not self.frame then return end

    -- Update model
    self.frame.model:Undress()
    local _, link = GetItemInfo(itemID)
    local tryOnArg = link or ("item:" .. itemID)
    C_Timer.After(0.05, function()
        pcall(self.frame.model.TryOn, self.frame.model, tryOnArg)
    end)

    -- Update item name
    local name, _, quality, _, _, _, _, _, _, icon = GetItemInfo(itemID)
    if name then
        local color = QUALITY_COLORS[quality or 1] or "|cffffffff"
        self.frame.itemName:SetText(color .. name .. "|r")
    end

    -- Update collection badge
    local isCollected = false
    local appID = ETB:ResolveVisualIDFromItemID(itemID)
    if appID then
        -- Check if any source for this appearance is collected
        local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, appID)
        if ok and sources then
            for _, s in ipairs(sources) do
                if s.isCollected then isCollected = true; break end
            end
        end
    end

    if isCollected then
        self.frame.collectionBadge:SetText("|cff00ff00\226\156\147 Collected|r")
    else
        self.frame.collectionBadge:SetText("|cffff6666\226\156\151 Not Collected|r")
    end

    -- Highlight the clicked row
    for _, row in ipairs(self.frame.itemRows) do
        if row.itemID == itemID then
            row.selectedBg:Show()
        else
            row.selectedBg:Hide()
        end
    end
end

-- ============================================================================
-- HIDE / TOGGLE
-- ============================================================================

function ADP:Hide()
    if self.overlay then self.overlay:Hide() end
    if self.frame then self.frame:Hide() end
    self.currentItem = nil
end

function ADP:Toggle(item)
    if self.frame and self.frame:IsShown() and self.currentItem == item then
        self:Hide()
    else
        self:Show(item)
    end
end

ETB:Debug("AppearanceDetail", "AppearanceDetailPanel.lua loaded")
