--[[
============================================================================
ETB v10.0 - ItemRow.lua
============================================================================
Reusable item row component with frame pooling.
============================================================================
]]--

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

ETB.ItemRow = {
    pool = {},
    active = {},
    nextID = 1,
}

local ItemRow = ETB.ItemRow

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

function ItemRow:Acquire(parent)
    local frame = table.remove(self.pool)
    if not frame then
        frame = self:CreateFrame(parent)
    else
        frame:SetParent(parent)
    end
    frame:Show()
    self.active[frame] = true
    return frame
end

function ItemRow:Release(frame)
    if not frame then return end
    frame:Hide()
    frame:ClearAllPoints()
    frame.itemData = nil
    self.active[frame] = nil
    self.pool[#self.pool + 1] = frame
end

function ItemRow:ReleaseAll()
    for frame in pairs(self.active) do
        self:Release(frame)
    end
end

function ItemRow:GetPoolStats()
    local active = 0
    for _ in pairs(self.active) do active = active + 1 end
    return { pool = #self.pool, active = active, total = #self.pool + active }
end

-- =============================================================================
-- CREATE FRAME
-- =============================================================================

function ItemRow:CreateFrame(parent)
    local id = self.nextID
    self.nextID = self.nextID + 1
    
    local f = CreateFrame("Button", "ETBItemRow" .. id, parent)
    f:SetSize(440, 32)
    f:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
    
    f.bg = f:CreateTexture(nil, "BACKGROUND")
    f.bg:SetAllPoints()
    f.bg:SetColorTexture(0.1, 0.1, 0.1, 0.5)
    
    f.collectedBar = f:CreateTexture(nil, "ARTWORK")
    f.collectedBar:SetSize(3, 28)
    f.collectedBar:SetPoint("LEFT", 0, 0)
    
    f.icon = f:CreateTexture(nil, "ARTWORK")
    f.icon:SetSize(24, 24)
    f.icon:SetPoint("LEFT", 5, 0)
    f.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    
    f.iconBorder = f:CreateTexture(nil, "OVERLAY")
    f.iconBorder:SetSize(26, 26)
    f.iconBorder:SetPoint("CENTER", f.icon, "CENTER")
    f.iconBorder:SetTexture("Interface\\Common\\WhiteIconFrame")
    
    f.name = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.name:SetPoint("LEFT", f.icon, "RIGHT", 5, 0)
    f.name:SetPoint("RIGHT", f, "RIGHT", -170, 0)
    f.name:SetJustifyH("LEFT")
    f.name:SetWordWrap(false)
    
    f.source = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.source:SetPoint("RIGHT", f.wishlistBtn, "LEFT", -6, 0)
    f.source:SetWidth(90)
    f.source:SetJustifyH("RIGHT")
    f.source:SetTextColor(0.5, 0.5, 0.5)
    f.source:SetWordWrap(false)
    
    f.status = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.status:SetPoint("RIGHT", -10, 0)
    
    f.favStar = f:CreateTexture(nil, "OVERLAY")
    f.favStar:SetSize(14, 14)
    f.favStar:SetPoint("RIGHT", f.status, "LEFT", -4, 0)
    f.favStar:SetTexture("Interface\\Common\\ReputationStar")
    f.favStar:SetTexCoord(0.5, 1, 0, 0.5)
    f.favStar:Hide()
    
    -- Wishlist button (clickable)
    f.wishlistBtn = CreateFrame("Button", nil, f)
    f.wishlistBtn:SetSize(16, 16)
    f.wishlistBtn:SetPoint("RIGHT", f.favStar, "LEFT", -3, 0)
    f.wishlistBtn.icon = f.wishlistBtn:CreateTexture(nil, "OVERLAY")
    f.wishlistBtn.icon:SetAllPoints()
    f.wishlistBtn.icon:SetTexture("Interface\\Buttons\\UI-GuildButton-PublicNote-Up")
    f.wishlistBtn.icon:SetVertexColor(0.4, 0.4, 0.4)
    f.wishlistBtn:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight", "ADD")
    f.wishlistBtn:SetScript("OnClick", function(self)
        local item = self:GetParent().itemData
        if item and item.itemID and ETB.Wishlist then
            local added = ETB.Wishlist:ToggleItem(item.itemID)
            if added then
                self.icon:SetVertexColor(1, 0.82, 0)
                self:SetAlpha(1)
                ETB:Print("|cffffcc00Wishlist:|r Added " .. (item.name or "item"))
            else
                self.icon:SetVertexColor(0.5, 0.5, 0.5)
                self:SetAlpha(0.5)
                ETB:Print("|cffffcc00Wishlist:|r Removed " .. (item.name or "item"))
            end
        end
    end)
    f.wishlistBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        local item = self:GetParent().itemData
        local onList = item and ETB.Wishlist and ETB.Wishlist:HasItem(item.itemID)
        if onList then
            GameTooltip:AddLine("Click to remove from wishlist")
        else
            GameTooltip:AddLine("Click to add to wishlist")
        end
        GameTooltip:Show()
    end)
    f.wishlistBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    
    -- Class usability indicator
    f.classIndicator = f:CreateTexture(nil, "OVERLAY")
    f.classIndicator:SetSize(14, 14)
    f.classIndicator:SetPoint("RIGHT", f.wishlistBtn, "LEFT", -3, 0)
    f.classIndicator:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up")
    f.classIndicator:Hide()
    
    -- Alternative items indicator/button (next to name)
    f.altButton = CreateFrame("Button", nil, f)
    f.altButton:SetSize(18, 18)
    f.altButton:SetPoint("LEFT", f.name, "RIGHT", 2, 0)
    f.altButton:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
    f.altButton:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down")
    f.altButton:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight", "ADD")
    f.altButton:SetScript("OnClick", function(self, button)
        if button == "LeftButton" then
            ItemRow:CycleAlternative(self:GetParent())
        end
    end)
    f.altButton:SetScript("OnEnter", function(self)
        local item = self:GetParent().itemData
        if item and item.hasAlternatives then
            GameTooltip:SetOwner(self, "ANCHOR_LEFT")
            GameTooltip:SetText("Alternate Items", 1, 1, 1)
            GameTooltip:AddLine(string.format("Click to cycle through %d items with this appearance", #item.alternatives), 0.8, 0.8, 0.8)
            GameTooltip:AddLine(" ")
            for i, alt in ipairs(item.alternatives) do
                local marker = (i == item.selectedAlternativeIndex) and "|cff00ff00►|r " or "   "
                local color = ETB:GetQualityColor(alt.quality)
                GameTooltip:AddLine(marker .. color .. (alt.name or "Unknown") .. "|r", 1, 1, 1)
            end
            GameTooltip:Show()
        end
    end)
    f.altButton:SetScript("OnLeave", function() GameTooltip:Hide() end)
    f.altButton:Hide()
    
    f:SetScript("OnEnter", function(self) ItemRow:OnEnter(self) end)
    f:SetScript("OnLeave", function()
        GameTooltip:Hide()
        if ETB.ModelPreview then
            -- v15.8.6: clear pending hover so any in-flight timer becomes a no-op
            ETB.ModelPreview._pendingHoverItemID = nil
            ETB.ModelPreview:Hide()
        end
    end)
    f:SetScript("OnClick", function(self, btn) ItemRow:OnClick(self, btn) end)

    f:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    -- v14.6.0 Feature 1.3: dim overlay shown when this appearance is hidden
    -- and the user has "show hidden" turned on (otherwise the row wouldn't
    -- be present at all). Helps distinguish hidden from non-hidden when both
    -- are visible in the list.
    f.hiddenOverlay = f:CreateTexture(nil, "OVERLAY")
    f.hiddenOverlay:SetAllPoints()
    f.hiddenOverlay:SetColorTexture(0, 0, 0, 0.45)
    f.hiddenOverlay:Hide()

    return f
end

-- =============================================================================
-- UPDATE
-- =============================================================================

function ItemRow:Update(frame, item, index)
    if not frame or not item then return end
    
    frame.itemData = item
    frame.index = index

    -- v14.6.0 Feature 1.3: dim hidden appearances when shown
    if frame.hiddenOverlay then
        if ETB.HiddenAppearances and ETB.HiddenAppearances:IsHidden(item.itemID) then
            frame.hiddenOverlay:Show()
        else
            frame.hiddenOverlay:Hide()
        end
    end
    
    frame.bg:SetColorTexture(index % 2 == 0 and 0.12 or 0.08, index % 2 == 0 and 0.12 or 0.08, index % 2 == 0 and 0.14 or 0.09, 0.5)
    if item.isCollected then
        frame.collectedBar:SetColorTexture(0, 1, 0.5, 0.9)
    else
        frame.collectedBar:SetColorTexture(1, 0.25, 0.25, 0.8)
    end
    
    frame.icon:SetTexture(item.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
    
    local r, g, b = ETB:GetQualityRGB(item.quality)
    frame.iconBorder:SetVertexColor(r, g, b)
    
    -- Handle name - convert to string and check if it's actually a name or just itemID
    local displayName = item.name
    
    -- If name is missing or invalid, try to get it
    if not displayName or displayName == "" or displayName == "Loading..." or displayName == "Unknown" or type(displayName) == "number" then
        -- Try GetItemInfo first (might be cached)
        if item.itemID then
            local name = GetItemInfo(item.itemID)
            if name and name ~= "" then
                displayName = name
                -- Update the item object
                item.name = name
                -- Also update cache
                if ETB.BlizzardData and ETB.BlizzardData.itemCache then
                    local cached = ETB.BlizzardData.itemCache:Get(item.itemID)
                    if cached then
                        cached.name = name
                    end
                end
            else
                displayName = "Loading..."
                -- Request item info if not already pending
                if ETB.BlizzardData and not ETB.BlizzardData.pendingItemInfo[item.itemID] then
                    ETB.BlizzardData.pendingItemInfo[item.itemID] = true
                    GetItemInfo(item.itemID)  -- Request async
                end
            end
        else
            displayName = "Loading..."
        end
    else
        displayName = tostring(displayName)
    end
    
    frame.name:SetText(ETB:GetQualityColor(item.quality) .. displayName .. "|r")
    
    -- Source info (fills gap between name and right-side icons)
    local srcText = ""
    if item.sourceType then
        local SOURCE_LABELS = {
            BOSS = item.isRaid and "Raid" or "Dungeon",
            QUEST = "Quest",
            VENDOR = "Vendor",
            CRAFTED = "Crafted",
            PVP = "PvP",
            WORLD_DROP = "World Drop",
            ACHIEVEMENT = "Achievement",
            PROFESSION = "Crafted",
        }
        srcText = SOURCE_LABELS[item.sourceType] or item.sourceType
    elseif item.sourceID and C_TransmogCollection and C_TransmogCollection.GetSourceInfo then
        local ok, info = pcall(C_TransmogCollection.GetSourceInfo, item.sourceID)
        if ok and info and info.sourceType then
            local BLIZZ_SOURCE = {
                [1] = "Boss Drop",
                [2] = "Quest",
                [3] = "Vendor",
                [4] = "World Drop",
                [5] = "Achievement",
                [6] = "Crafted",
            }
            srcText = BLIZZ_SOURCE[info.sourceType] or ""
        end
    end
    frame.source:SetText("|cffaaaaaa" .. srcText .. "|r")
    
    frame.status:SetText(item.isCollected and "|cff00ff00✓|r" or "|cffff6666✗|r")
    
    -- Favorites (always visible - dim when not starred, gold when starred)
    local isFav = ETB.FavoritesManager and ETB.FavoritesManager:IsFavorite(item.itemID)
    if isFav then
        frame.favStar:SetVertexColor(1, 0.82, 0)
        frame.favStar:SetAlpha(1)
    else
        frame.favStar:SetVertexColor(0.5, 0.5, 0.5)
        frame.favStar:SetAlpha(0.5)
    end
    frame.favStar:Show()
    
    -- Wishlist (always visible - dim when not listed, gold when listed)
    local isWL = ETB.Wishlist and ETB.Wishlist:HasItem(item.itemID)
    if isWL then
        frame.wishlistBtn.icon:SetVertexColor(1, 0.82, 0)
        frame.wishlistBtn:SetAlpha(1)
    else
        frame.wishlistBtn.icon:SetVertexColor(0.5, 0.5, 0.5)
        frame.wishlistBtn:SetAlpha(0.5)
    end
    
    -- Class usability indicator - shows if active profile cannot transmog this item
    local canTransmog = true
    if item.armorType then
        canTransmog = ETB.BlizzardData and ETB.BlizzardData:CanPlayerWearArmorType(item.armorType) or true
    elseif item.weaponType then
        canTransmog = ETB.BlizzardData and ETB.BlizzardData:CanPlayerUseWeaponType(item.weaponType) or true
    end
    
    if not canTransmog then
        frame.classIndicator:Show()
        -- Also dim the row slightly to make it visually distinct
        frame.bg:SetColorTexture(0.2, 0.1, 0.1, 0.5)  -- Reddish tint
    else
        frame.classIndicator:Hide()
    end
    
    -- Show alternative items button if there are alternatives
    if item.hasAlternatives and item.alternatives and #item.alternatives > 1 then
        frame.altButton:Show()
    else
        frame.altButton:Hide()
    end
end

-- =============================================================================
-- CYCLE ALTERNATIVE ITEMS
-- =============================================================================

function ItemRow:CycleAlternative(frame)
    local item = frame.itemData
    if not item or not item.hasAlternatives or not item.alternatives then return end
    
    -- Cycle to next alternative
    local currentIndex = item.selectedAlternativeIndex or 1
    local nextIndex = (currentIndex % #item.alternatives) + 1
    item.selectedAlternativeIndex = nextIndex
    
    -- Replace current item data with the selected alternative
    local altItem = item.alternatives[nextIndex]
    if altItem then
        -- Copy all properties from alternative to current item
        for key, value in pairs(altItem) do
            if key ~= "alternatives" and key ~= "selectedAlternativeIndex" and key ~= "hasAlternatives" then
                item[key] = value
            end
        end
    end
    
    -- Update the display
    ItemRow:Update(frame, item, frame.index)
    
    -- Update preview if shown
    if ETB.ModelPreview then
        ETB.ModelPreview:PreviewItem(item)
    end
end

-- =============================================================================
-- EVENTS
-- =============================================================================

function ItemRow:OnEnter(frame)
    local item = frame.itemData
    if not item then return end

    -- v15.8.6: Lag mitigation. The combination of hover preview + scrolling
    -- through hundreds of rows fires a DressUpModel TryOn (an expensive
    -- graphics op on MoP 5.4.8) on every transient hover. Even with the
    -- 0.05s debounce, fast scroll-pasted rows stack up timers. Track a
    -- "pending hover" itemID on the module so a new hover supersedes the
    -- pending one and the timer can short-circuit.

    -- Check if hover preview is enabled
    local showHoverPreview = ETB.Settings:Get("preview.showOnHover")

    -- v15.8.7: When Shift is held during hover, defer to the tooltip path
    -- regardless of the hover-preview setting. The user is trying to use
    -- Blizzard's native item-compare (Shift+hover spawns a side-by-side
    -- compare tooltip from any visible item link, e.g. an item they just
    -- chat-linked). Our floating ModelPreview would compete and overlap.
    -- Also dismiss any currently-showing hover preview so the tooltip
    -- isn't underneath it.
    if IsShiftKeyDown and IsShiftKeyDown() then
        if ETB.ModelPreview then
            ETB.ModelPreview._pendingHoverItemID = nil
            ETB.ModelPreview:Hide()
        end
        showHoverPreview = false  -- Force tooltip path below
    end

    if showHoverPreview and ETB.ModelPreview then
        -- Show MINIMAL tooltip (just collection status) since preview will show the item
        GameTooltip:SetOwner(frame, "ANCHOR_NONE")  -- Hide from view
        GameTooltip:ClearLines()

        -- v15.8.6: stamp this hover; the timer below will only proceed if
        -- the user is still on this same row when it fires. Previously the
        -- check was 'frame.itemData == item' but item is the captured value
        -- at hover time — so it always passed once the timer fired, even if
        -- the user had moved on. Tracking the pending itemID on the module
        -- itself lets us cancel-by-supersession.
        ETB.ModelPreview._pendingHoverItemID = item.itemID
        local thisHoverItemID = item.itemID

        -- v15.8.6: bumped from 0.05s to 0.15s. 50ms is too short to filter
        -- out scroll-induced transient hovers; 150ms is the eye-blink
        -- threshold and feels instant to a user who's intentionally
        -- pausing on a row, but it suppresses the burst of timers fired
        -- when scrolling through 30 rows in a second.
        C_Timer.After(0.15, function()
            -- Bail if a different row has been hovered since
            if ETB.ModelPreview._pendingHoverItemID ~= thisHoverItemID then return end
            -- Bail if the user has moved off this row
            if not frame:IsMouseOver() then return end
            -- Bail if the main frame has been hidden
            if not (ETB.MainFrame and ETB.MainFrame.frame and ETB.MainFrame.frame:IsVisible()) then
                return
            end
            ETB.ModelPreview:PreviewItem(item)
        end)
    else
        -- v15.8.6: Anchor tooltip to ANCHOR_LEFT (was ANCHOR_RIGHT). The
        -- right side of the item row is where the Live Preview model lives,
        -- so a right-anchored tooltip covers it — the #1 complaint from
        -- in-game testers. The filter panel is on the LEFT of the rows with
        -- plenty of empty space below it, so anchoring there keeps both the
        -- tooltip and the preview visible at the same time.
        GameTooltip:SetOwner(frame, "ANCHOR_LEFT")
        if item.itemID then
            local itemName, itemLink = GetItemInfo(item.itemID)
            if itemLink and itemLink ~= "" then
                if itemLink:match("^|c%x%x%x%x%x%x%x%x|Hitem:") then
                    pcall(GameTooltip.SetHyperlink, GameTooltip, itemLink)
                else
                    ETB:SafeSetTooltipItem(GameTooltip, item.itemID)
                end
            else
                ETB:SafeSetTooltipItem(GameTooltip, item.itemID)
                GetItemInfo(item.itemID)
            end
        end
        
        GameTooltip:AddLine(" ")
        GameTooltip:AddDoubleLine("ItemID:", item.itemID or "?", 1, 0.82, 0, 1, 1, 1)
        local vid = ETB:ResolveVisualID(item)
        if vid then
            GameTooltip:AddDoubleLine("VisualID:", vid, 1, 0.82, 0, 1, 1, 1)
        end
        if item.armorType then
            GameTooltip:AddDoubleLine("Armor Type:", item.armorType, 1, 0.82, 0, 1, 1, 1)
        end
        
        -- Source info is handled by TooltipEnhancer hook (FormatSourceTooltip)
        -- which fires automatically on SetHyperlink above - matching SourcesModal data
        
        GameTooltip:AddLine(" ")
        if item.isCollected then
            GameTooltip:AddLine("|cff00ff00Appearance Collected|r")
            -- Show which item was collected to learn this appearance
            if item.alternatives and #item.alternatives > 1 then
                for _, alt in ipairs(item.alternatives) do
                    if alt.isCollected and alt.name and alt.name ~= "Loading..." then
                        GameTooltip:AddLine("|cffffd100Collected via:|r " .. (alt.name or "Unknown"), 0.8, 0.8, 0.8)
                        break
                    end
                end
            elseif item.isCollected and item.name and item.name ~= "Loading..." then
                GameTooltip:AddLine("|cffffd100Collected via:|r " .. item.name, 0.8, 0.8, 0.8)
            end
        else
            GameTooltip:AddLine("|cffff6666Not Collected|r")
        end
        
        -- Show if active profile can transmog this item
        local canTransmog = true
        if item.armorType then
            canTransmog = ETB.BlizzardData and ETB.BlizzardData:CanPlayerWearArmorType(item.armorType) or true
        elseif item.weaponType then
            canTransmog = ETB.BlizzardData and ETB.BlizzardData:CanPlayerUseWeaponType(item.weaponType) or true
        end
        if not canTransmog then
            local activeClass = ETB.BlizzardData and ETB.BlizzardData:GetActiveClassName() or "Unknown"
            GameTooltip:AddLine("|cffff4444Cannot be transmogged by " .. activeClass .. "|r")
        end
        
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Left-click: Preview | Shift: Link | Alt: Favorite", 0.5, 0.5, 0.5)
        GameTooltip:AddLine("Right-click: Sources | Note icon: Wishlist", 0.5, 0.5, 0.5)
        
        GameTooltip:Show()
    end
end

function ItemRow:OnClick(frame, button)
    local item = frame.itemData
    if not item then return end

    -- v15.8.7: Helper to dismiss the hover preview popup when the user
    -- takes an action that shifts their attention OUT of ETB's visual
    -- focus — Shift+Click chat-link (they're comparing in chat-side
    -- tooltips), Ctrl+Click dressing-room (they're using Blizzard's UI).
    -- Without this the floating 280x420 ModelPreview frame stays put and
    -- overlaps whatever they do next (e.g. Blizzard's compare tooltip
    -- spawning to the right of the chat link). Also clear the pending
    -- hover so any in-flight timer is a no-op.
    local function dismissHoverPreview()
        if ETB.ModelPreview then
            ETB.ModelPreview._pendingHoverItemID = nil
            ETB.ModelPreview:Hide()
        end
        -- Also hide the regular tooltip — same overlap concern, even
        -- though the tooltip is now anchored LEFT (v15.8.6), the chat
        -- link will pull focus away from the row and the row tooltip
        -- becomes orphaned visual noise.
        GameTooltip:Hide()
    end

    if button == "LeftButton" then
        if IsShiftKeyDown() then
            -- Shift+Click: insert link into chat. User wants to compare
            -- via chat-link hover (Blizzard's compare-tooltip). Dismiss
            -- our hover preview first so it doesn't overlap.
            dismissHoverPreview()
            if item.itemID then
                local _, link = GetItemInfo(item.itemID)
                if link then ChatEdit_InsertLink(link) end
            end
        elseif IsControlKeyDown() then
            -- Ctrl+Click: open in Blizzard's dressing room. Same overlap
            -- concern — Blizzard's dressing room overlay is a big frame
            -- and our hover preview would float above it.
            dismissHoverPreview()
            if item.itemID and DressUpItemLink then
                DressUpItemLink("item:" .. item.itemID)
            end
        elseif IsAltKeyDown() then
            -- Alt+Click: toggle favorite. Stay on the row visually.
            if ETB.FavoritesManager then
                ETB.FavoritesManager:ToggleFavorite(item.itemID)
                ItemRow:Update(frame, item, frame.index)
            end
        else
            -- v15.8.1: Plain Left-click applies the appearance to the
            -- Live Preview model. Stay on the row visually — the user
            -- is iterating.
            if ETB.MainFrame and ETB.MainFrame.AddToLivePreview then
                ETB.MainFrame:AddToLivePreview(item)
            end
        end
    elseif button == "RightButton" then
        if IsControlKeyDown() then
            -- Ctrl+Right-click: toggle wishlist (stays on row)
            if ETB.Wishlist and item.itemID then
                local added = ETB.Wishlist:ToggleItem(item.itemID)
                if added then
                    ETB:Print("|cffffcc00Wishlist:|r Added " .. (item.name or "item"))
                else
                    ETB:Print("|cffffcc00Wishlist:|r Removed " .. (item.name or "item"))
                end
                ItemRow:Update(frame, item, frame.index)
            end
        elseif IsShiftKeyDown() then
            -- v14.6.0 Feature 1.3: Shift+Right-click toggles hide flag
            -- (stays on row)
            if ETB.HiddenAppearances and item.itemID then
                local nowHidden = ETB.HiddenAppearances:Toggle(item.itemID)
                ItemRow:Update(frame, item, frame.index)
                if ETB.EventBus then
                    ETB.EventBus:Fire("ETB_FILTERS_CHANGED")
                end
            end
        else
            -- v15.8.1: Plain right-click opens the Appearance Detail
            -- panel — also a window that may overlap the hover preview.
            -- Dismiss the floating preview so the panel doesn't compete
            -- for screen real estate.
            dismissHoverPreview()
            if ETB.AppearanceDetail then
                ETB.AppearanceDetail:Toggle(item)
            elseif ETB.SourcesModal then
                ETB.SourcesModal:Show(item)
            end
        end
    end
end

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