-- Housing Decor Guide - Decor Preview Module
-- Shows 3D preview of housing decor items with item details

local addonName, addon = ...

-- Module namespace
HDG_DecorPreview = HDG_DecorPreview or {}

-- Localization
local L = HDG_L or {}

-- Profession localization helper (English name -> L key)
local PROF_LOCALE_KEYS = {
    ["All"] = "PROF_ALL",
    ["Alchemy"] = "PROF_ALCHEMY",
    ["Blacksmithing"] = "PROF_BLACKSMITHING",
    ["Cooking"] = "PROF_COOKING",
    ["Enchanting"] = "PROF_ENCHANTING",
    ["Engineering"] = "PROF_ENGINEERING",
    ["Inscription"] = "PROF_INSCRIPTION",
    ["Jewelcrafting"] = "PROF_JEWELCRAFTING",
    ["Leatherworking"] = "PROF_LEATHERWORKING",
    ["Tailoring"] = "PROF_TAILORING",
}

local function GetLocalizedProfession(englishName)
    local key = PROF_LOCALE_KEYS[englishName]
    return key and L[key] or englishName
end

-- ============================================================================
-- THEME HELPERS (delegating to ThemeEngine canonical helpers)
-- ============================================================================

local HDG = _G.HousingDecorGuide
local function GetScheme() return HDG.Theme.GetCurrentScheme() end
local function RegisterWidget(w, t) HDG.Theme.RegisterWidget(w, t) end
local ApplyFont = function(fs, s, ft) HDG.Theme.ApplyFont(fs, s, ft) end


-- State color helpers for toggle buttons (return r, g, b, a)
local function GetStateActive() return HDG.Theme.GetStateActive() end
local function GetStateInactive() return HDG.Theme.GetStateInactive() end
local function GetStateHover() return HDG.Theme.GetStateHover() end

-- Local references (set during init)
local CONFIG
local mainFrame
local db

-- Module frame references
local previewPanel = nil
local selectedDecorID = nil  -- Use decorID for unique selection

-- Scroll position tracking
local scrollOffset = 0

-- Search debounce timer (prevents excessive RefreshList calls while typing)
local searchDebounceTimer = nil

-- GetAllDecorItems cache (avoids rebuilding item list with API calls on every refresh)
local decorItemsCache = nil
local decorItemsCacheTime = 0
local DECOR_CACHE_TTL = 60  -- seconds

-- Text width measurement helper (reusable, avoids creating/destroying frames)
local textMeasureFrame = nil
local function GetTextWidth(text, fontObject)
    if not textMeasureFrame then
        textMeasureFrame = UIParent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        textMeasureFrame:Hide()
    end
    textMeasureFrame:SetText(text)
    return textMeasureFrame:GetStringWidth()
end

-- Row pool for item list (optimization: reuse frames instead of creating new ones)
local rowPool

-- Helper: Normalize expansion name - use centralized HDG_ExpansionData module
local function NormalizeExpansionName(expansion)
    return HDG_ExpansionData.NormalizeToDisplayName(expansion)
end

-- Collection check uses centralized HDG_Data.IsDecorCollected(decorID)

-- Lazy-built lookup: decorID → added timestamp from AllDecorDB for released-item filtering
local knownDecorIDs = nil
local function GetKnownDecorIDs()
    if knownDecorIDs then return knownDecorIDs end
    knownDecorIDs = {}
    if HDG_AllDecorDB then
        for _, entry in pairs(HDG_AllDecorDB) do
            knownDecorIDs[entry[1]] = entry[5] or 0 -- decorID → added timestamp
        end
    end
    return knownDecorIDs
end

-- Helper: Check if item passes all current filters
local function ItemPassesFilters(itemData)
    local searchFilter = HDG_DecorPreview.searchFilter or ""
    local categoryFilter = HDG_DecorPreview.categoryFilter
    local professionFilter = HDG_DecorPreview.professionFilter
    local uncollectedOnly = HDG_DecorPreview.uncollectedOnly or false
    local tagFilters = HDG_DecorPreview.tagFilters or {}
    local hasTagFilters = next(tagFilters) ~= nil

    -- Apply search filter (check both localized name and English name from database)
    local itemName = (itemData.name or ""):lower()
    local englishName = (itemData.englishName or ""):lower()
    local matchesSearch = (searchFilter == "" or itemName:find(searchFilter, 1, true) or englishName:find(searchFilter, 1, true))
    if not matchesSearch then return false end

    -- Apply category filter
    if categoryFilter and itemData.categoryName ~= categoryFilter then return false end

    -- Apply profession filter
    -- When a profession is selected, only show items from that profession
    -- Items without a profession (non-craftable catalog items) are filtered out when a profession filter is active
    if professionFilter then
        if not itemData.profession or itemData.profession ~= professionFilter then
            return false
        end
    end

    -- Apply uncollected filter
    if uncollectedOnly then
        local isCollected = HDG_Data.IsDecorCollected(itemData.decorID)
        if isCollected then return false end
    end

    -- Apply tag filters (item must have at least one of the selected tags)
    if hasTagFilters then
        local matchesTags = false
        if itemData.tags then
            for _, tag in ipairs(itemData.tags) do
                if tagFilters[tag] then
                    matchesTags = true
                    break
                end
            end
        end
        if not matchesTags then return false end
    end

    -- Hide unreleased: decorID must exist in AllDecorDB with a non-future timestamp
    if HDG_DecorPreview.hideUnreleased then
        if itemData.name == "Unreleased" then return false end
        local known = GetKnownDecorIDs()
        if itemData.decorID then
            local addedTS = known[itemData.decorID]
            if not addedTS or addedTS > time() then return false end
        end
    end

    return true
end

-- Helper: Build recipe object for adding to shopping list queue
local function BuildRecipeForQueue(itemData)
    -- itemData can come from row.itemData or recipeData from HDG_DecorDB
    return {
        id = itemData.itemID,
        name = itemData.name,
        spell = itemData.spellID,
        reagents = itemData.reagents,
        expansion = NormalizeExpansionName(itemData.expansion),
        profession = itemData.profession
    }
end

-- Get item info with caching
local itemCache = {}
local function GetCachedItemInfo(itemID)
    if itemCache[itemID] then
        return unpack(itemCache[itemID])
    end
    
    local name, link, quality, _, _, _, _, _, _, icon = C_Item.GetItemInfo(itemID)
    if name then
        itemCache[itemID] = {name, link, quality, icon}
        return name, link, quality, icon
    end
    return nil, nil, nil, nil
end

-- Get quality color (theme-aware: uses Solarized colors for Uncommon/Rare)
local function GetQualityColor(quality)
    if quality then
        return HDG.Theme.GetQualityRGB(quality)
    end
    return 1, 1, 1
end

-- Keyword-based fallback categorization for items without API category data
-- Cache results to avoid repeated string matching
local fallbackCategoryCache = {}

local function GetFallbackCategoryUncached(name)
    -- Check for subcategory keywords (more specific first)
    -- Furnishings subcategories
    if name:find("bookcase") or name:find("shelf") or name:find("shelves") or name:find("cabinet") or name:find("chest") then
        return 1, 6 -- Furnishings > Storage
    elseif name:find("table") or name:find("desk") then
        return 1, 5 -- Furnishings > Tables and Desks
    elseif name:find("chair") or name:find("seat") or name:find("bench") or name:find("stool") or name:find("throne") then
        return 1, 1 -- Furnishings > Seating
    elseif name:find("bed") or name:find("cot") then
        return 1, 2 -- Furnishings > Beds

    -- Lighting subcategories
    elseif name:find("brazier") or name:find("chandelier") or name:find("torch") then
        return 4, 16 -- Lighting > Large Lights
    elseif name:find("candle") or name:find("lamp") or name:find("lantern") then
        return 4, 19 -- Lighting > Small Lights
    elseif name:find("ceiling light") then
        return 4, 18 -- Lighting > Ceiling Lights
    elseif name:find("wall light") or name:find("sconce") then
        return 4, 17 -- Lighting > Wall Lights

    -- Structural subcategories
    elseif name:find("door") or name:find("gate") then
        return 2, 3 -- Structural > Doors
    elseif name:find("wall") or name:find("column") or name:find("pillar") then
        return 2, 4 -- Structural > Walls and Columns
    elseif name:find("window") then
        return 2, 8 -- Structural > Windows
    elseif name:find("floor") or name:find("tile") then
        return 2, 14 -- Structural > Floor

    -- Accents subcategories
    elseif name:find("painting") or name:find("banner") or name:find("tapestry") or name:find("portrait") then
        return 3, 12 -- Accents > Wall Hangings
    elseif name:find("ornament") or name:find("statue") or name:find("sculpture") then
        return 3, 11 -- Accents > Ornamental

    -- Nature subcategories
    elseif name:find("tree") or name:find("plant") then
        return 6, 25 -- Nature > Large Foliage
    elseif name:find("bush") or name:find("shrub") then
        return 6, 27 -- Nature > Bushes
    elseif name:find("flower") or name:find("grass") or name:find("vine") then
        return 6, 26 -- Nature > Small Foliage

    -- Functional subcategories
    elseif name:find("food") or name:find("drink") or name:find("meal") or name:find("plate") then
        return 5, 13 -- Functional > Food and Drink

    -- Broad category fallbacks (no subcategory)
    elseif name:find("furniture") then
        return 1, nil -- Furnishings
    elseif name:find("light") then
        return 4, nil -- Lighting
    elseif name:find("structure") then
        return 2, nil -- Structural
    elseif name:find("decoration") or name:find("decor") then
        return 3, nil -- Accents
    end

    -- No match found
    return nil, nil
end

local function GetFallbackCategory(itemName)
    if not itemName then return nil, nil end

    -- Check cache first
    local cached = fallbackCategoryCache[itemName]
    if cached then
        return cached[1], cached[2]
    end

    -- Compute and cache result
    local name = itemName:lower()
    local catID, subID = GetFallbackCategoryUncached(name)
    fallbackCategoryCache[itemName] = { catID, subID }

    return catID, subID
end

-- Build list of all decor items from the player's housing catalog
-- Cache for collected decor items (populated asynchronously)
local collectedDecorCache = nil
local collectedDecorCacheTime = 0
local collectionSearchInProgress = false

-- Get CACHE_TTL from CONFIG (accessed after init)
local function GetCacheTTL()
    return (CONFIG and CONFIG.PREVIEW_TAB and CONFIG.PREVIEW_TAB.CACHE_TTL) or 300
end

-- Process search results into item list
-- Note: C_HousingCatalog API does NOT return itemID - we use name/iconTexture/recordID instead
local function ProcessSearchResults(catalogSearcher)
    local items = {}

    -- Try GetCatalogSearchResults first (Blizzard's method), fall back to GetAllSearchItems
    local allItems = nil
    if catalogSearcher.GetCatalogSearchResults then
        allItems = catalogSearcher:GetCatalogSearchResults()
    end
    if not allItems or #allItems == 0 then
        if catalogSearcher.GetAllSearchItems then
            allItems = catalogSearcher:GetAllSearchItems()
        end
    end
    if not allItems then
        return items
    end

    -- Build reverse lookup from HDG_DecorDB: decorID -> recipe data
    local decorToRecipe = {}
    if HDG_DecorDB then
        for spellID, recipe in pairs(HDG_DecorDB) do
            if recipe.decorID then
                decorToRecipe[recipe.decorID] = {
                    itemID = recipe.itemID,
                    spellID = spellID,
                    profession = recipe.profession,
                    expansion = recipe.expansion,
                    reagents = recipe.reagents,
                    name = recipe.name,
                }
            end
        end
    end

    -- Process each entry - handle both entryID objects and raw results
    for _, entry in ipairs(allItems) do
        local entryInfo = nil
        local recordID = nil

        -- Determine entry format and get entryInfo
        if entry and entry.recordID then
            -- Entry is an entryID object (from GetAllSearchItems or GetCatalogSearchResults)
            recordID = entry.recordID
            -- Try GetCatalogEntryInfo first (Blizzard's method), fall back to GetCatalogEntryInfoByRecordID
            local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfo, entry)
            if ok and info then
                entryInfo = info
            else
                local entryType = entry.entryType or 1
                ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, entryType, recordID, true)
                if ok and info then
                    entryInfo = info
                end
            end
        end

        if entryInfo and entryInfo.name and recordID then
            -- Get tags
            local itemTags = {}
            if entryInfo.dataTagsByID then
                for _, tagName in pairs(entryInfo.dataTagsByID) do
                    table.insert(itemTags, tagName)
                end
            end

            -- Get category info from API
            local categoryName = nil
            local subcategoryName = nil
            local categoryID = nil
            local subcategoryID = nil
            if entryInfo.categoryIDs and entryInfo.categoryIDs[1] then
                categoryID = entryInfo.categoryIDs[1]
                if C_HousingCatalog.GetCatalogCategoryInfo then
                    local catInfo = C_HousingCatalog.GetCatalogCategoryInfo(categoryID)
                    if catInfo and catInfo.name then
                        categoryName = catInfo.name
                    end
                end
            end
            if entryInfo.subcategoryIDs and entryInfo.subcategoryIDs[1] then
                subcategoryID = entryInfo.subcategoryIDs[1]
                if C_HousingCatalog.GetCatalogSubcategoryInfo then
                    local subInfo = C_HousingCatalog.GetCatalogSubcategoryInfo(subcategoryID)
                    if subInfo and subInfo.name then
                        subcategoryName = subInfo.name
                    end
                end
            end

            -- Check if this is a craftable recipe from our database
            local recipeData = decorToRecipe[recordID]
            local isCraftable = recipeData ~= nil
            local itemID = recipeData and recipeData.itemID or entryInfo.itemID
            local profession = recipeData and recipeData.profession or nil  -- nil for non-craftable
            local expansion = recipeData and recipeData.expansion or "Unknown"
            local spellID = recipeData and recipeData.spellID or nil
            local reagents = recipeData and recipeData.reagents or nil

            table.insert(items, {
                itemID = itemID,  -- May be nil for non-craftable items
                decorID = recordID,
                name = entryInfo.name,
                englishName = recipeData and recipeData.name or nil,  -- English name from DB for search
                iconTexture = entryInfo.iconTexture,  -- Use API icon when no itemID
                profession = profession,  -- nil for non-craftable items
                expansion = expansion,
                spellID = spellID,
                reagents = reagents,
                tags = itemTags,
                categoryName = categoryName,
                subcategoryName = subcategoryName,
                categoryID = categoryID,
                subcategoryID = subcategoryID,
                isCatalogItem = not isCraftable,
            })
        end
    end

    return items
end

-- Trigger async search for ALL decor (results cached for future calls)
-- Uses Blizzard's catalog API to get all decor items regardless of collection status
local function TriggerAllDecorSearch()
    if collectionSearchInProgress then
        return
    end
    if not C_HousingCatalog or not C_HousingCatalog.CreateCatalogSearcher then
        return
    end

    collectionSearchInProgress = true

    local ok, catalogSearcher = pcall(C_HousingCatalog.CreateCatalogSearcher)
    if not ok or not catalogSearcher then
        collectionSearchInProgress = false
        return
    end

    -- Configure searcher for ALL decor items (not just owned)
    -- This matches Blizzard's HousingCatalogFrameMixin approach
    -- Wrap in pcall in case API methods changed
    local configOk = pcall(function()
        if catalogSearcher.SetOwnedOnly then
            catalogSearcher:SetOwnedOnly(false)  -- Get ALL items, not just owned
        end
        if catalogSearcher.SetCollected then
            catalogSearcher:SetCollected(true)   -- Include collected
        end
        if catalogSearcher.SetUncollected then
            catalogSearcher:SetUncollected(true) -- Include uncollected
        end
        -- Filter to BasicDecor mode to exclude rooms (like Blizzard's catalog does)
        if catalogSearcher.SetEditorModeContext and Enum and Enum.HouseEditorMode then
            catalogSearcher:SetEditorModeContext(Enum.HouseEditorMode.BasicDecor)
        end
        -- Don't include market entries (cash shop items)
        if catalogSearcher.SetIncludeMarketEntries then
            catalogSearcher:SetIncludeMarketEntries(false)
        end
    end)

    if not configOk then
        collectionSearchInProgress = false
        if catalogSearcher.Release then
            pcall(catalogSearcher.Release, catalogSearcher)
        end
        return
    end

    -- Set callback for when results are ready
    local callbackOk = pcall(function()
        catalogSearcher:SetResultsUpdatedCallback(function()
            local items = ProcessSearchResults(catalogSearcher)

            -- Update cache
            collectedDecorCache = items
            collectedDecorCacheTime = time()
            collectionSearchInProgress = false

            -- Release searcher
            if catalogSearcher.Release then
                pcall(catalogSearcher.Release, catalogSearcher)
            end

            -- Refresh the UI
            if HDG_DecorPreview.RefreshList then
                HDG_DecorPreview:RefreshList()
            end
            if HDG_DecorPreview.RefreshTagFilters then
                HDG_DecorPreview:RefreshTagFilters()
            end
        end)
    end)

    if not callbackOk then
        collectionSearchInProgress = false
        if catalogSearcher.Release then
            pcall(catalogSearcher.Release, catalogSearcher)
        end
        return
    end

    -- Start the async search
    local searchOk = pcall(function()
        catalogSearcher:RunSearch()
    end)

    if not searchOk then
        collectionSearchInProgress = false
        if catalogSearcher.Release then
            pcall(catalogSearcher.Release, catalogSearcher)
        end
    end
end

-- Get all decor items from cache (triggers async refresh if needed)
local function GetAllDecorFromAPI()
    -- Check if cache is valid
    local now = time()
    if collectedDecorCache and (now - collectedDecorCacheTime) < GetCacheTTL() then
        return collectedDecorCache
    end

    -- Trigger async search (will update cache and refresh UI when done)
    TriggerAllDecorSearch()

    -- Return cached data if available (even if stale), or empty list
    return collectedDecorCache or {}
end

-- Build list of all decor items from database (craftable recipes)
local function GetAllDecorItems()
    -- If "My Collection" mode is enabled, show collected items from API
    -- Otherwise show craftable items from HDG_DecorDB
    if HDG_DecorPreview.showAllDecor then
        return GetAllDecorFromAPI()
    end

    -- Check cache first (only for craftable items mode, not API mode)
    local now = GetTime()
    if decorItemsCache and (now - decorItemsCacheTime) < DECOR_CACHE_TTL then
        return decorItemsCache
    end

    -- Default: Show craftable items from our database (HDG_DecorDB)

    local items = {}

    -- Access global HDG_DecorDB directly (the raw data)
    if not HDG_DecorDB then
        return items
    end

    -- HDG_DecorDB is a table keyed by spellID with recipe data
    for spellID, recipe in pairs(HDG_DecorDB) do
        if recipe.itemID and recipe.decorID then
            -- Try to get category and tags from the housing catalog API
            local categoryName = nil
            local subcategoryName = nil
            local categoryID = nil
            local subcategoryID = nil
            local itemTags = {}

            if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
                local decorInfo = C_HousingCatalog.GetCatalogEntryInfoByItem(recipe.itemID, true)

                if decorInfo then
                    -- categoryIDs is a table like {1 = 4}
                    if decorInfo.categoryIDs and decorInfo.categoryIDs[1] then
                        categoryID = decorInfo.categoryIDs[1]
                        -- Get category name
                        if C_HousingCatalog.GetCatalogCategoryInfo then
                            local catInfo = C_HousingCatalog.GetCatalogCategoryInfo(categoryID)
                            if catInfo and catInfo.name then
                                categoryName = catInfo.name
                            end
                        end
                    end

                    -- subcategoryIDs is a table like {1 = 16}
                    if decorInfo.subcategoryIDs and decorInfo.subcategoryIDs[1] then
                        subcategoryID = decorInfo.subcategoryIDs[1]
                        -- Get subcategory name
                        if C_HousingCatalog.GetCatalogSubcategoryInfo then
                            local subInfo = C_HousingCatalog.GetCatalogSubcategoryInfo(subcategoryID)
                            if subInfo and subInfo.name then
                                subcategoryName = subInfo.name
                            end
                        end
                    end

                    -- Extract tags (size, faction, expansion, style)
                    if decorInfo.dataTagsByID then
                        for _, tagName in pairs(decorInfo.dataTagsByID) do
                            table.insert(itemTags, tagName)
                        end
                    end
                end
            end

            -- Fallback: If no category from API, try keyword-based categorization
            if not categoryID and recipe.name then
                local fallbackCatID, fallbackSubID = GetFallbackCategory(recipe.name)
                if fallbackCatID then
                    categoryID = fallbackCatID
                    -- Get category name
                    if C_HousingCatalog and C_HousingCatalog.GetCatalogCategoryInfo then
                        local catInfo = C_HousingCatalog.GetCatalogCategoryInfo(categoryID)
                        if catInfo and catInfo.name then
                            categoryName = catInfo.name
                        end
                    end

                    -- Get subcategory name if we have one
                    if fallbackSubID then
                        subcategoryID = fallbackSubID
                        if C_HousingCatalog and C_HousingCatalog.GetCatalogSubcategoryInfo then
                            local subInfo = C_HousingCatalog.GetCatalogSubcategoryInfo(subcategoryID)
                            if subInfo and subInfo.name then
                                subcategoryName = subInfo.name
                            end
                        end
                    end
                end
            end
            
            table.insert(items, {
                itemID = recipe.itemID,
                decorID = recipe.decorID,
                name = recipe.name,
                profession = recipe.profession or "Unknown",
                expansion = recipe.expansion or "Unknown",
                spellID = recipe.spellID or spellID,
                reagents = recipe.reagents,
                categoryID = categoryID,
                categoryName = categoryName or "Uncategorized",
                subcategoryID = subcategoryID,
                subcategoryName = subcategoryName,
                tags = itemTags,
            })
        end
    end
    
    -- Sort alphabetically by name
    table.sort(items, function(a, b)
        return (a.name or "") < (b.name or "")
    end)

    -- Cache the results
    decorItemsCache = items
    decorItemsCacheTime = GetTime()

    return items
end

-- Invalidate the decor items cache (call when collection changes)
function HDG_DecorPreview:InvalidateCache()
    decorItemsCache = nil
    decorItemsCacheTime = 0
end

-- Get unique categories from items
local function GetCategories(items)
    local categories = {}
    local seen = {}
    
    for _, item in ipairs(items) do
        local cat = item.categoryName or "Uncategorized"
        if not seen[cat] then
            seen[cat] = true
            table.insert(categories, cat)
        end
    end
    
    table.sort(categories)
    table.insert(categories, 1, "All Categories")
    
    return categories
end

-- Get unique professions from items
local function GetProfessions(items)
    local professions = {}
    local seen = {}

    for _, item in ipairs(items) do
        local prof = item.profession or "Unknown"
        if not seen[prof] then
            seen[prof] = true
            table.insert(professions, prof)
        end
    end

    table.sort(professions)
    table.insert(professions, 1, L["FILTER_ALL_PROFESSIONS"] or "All Professions")

    return professions
end

-- Get unique tags from items with counts
local function GetUniqueTags(items)
    local tagCounts = {}

    for _, item in ipairs(items) do
        if item.tags then
            for _, tag in ipairs(item.tags) do
                tagCounts[tag] = (tagCounts[tag] or 0) + 1
            end
        end
    end

    -- Convert to sorted array
    local tags = {}
    for tag, count in pairs(tagCounts) do
        table.insert(tags, { name = tag, count = count })
    end

    -- Sort by count descending (most common tags first)
    table.sort(tags, function(a, b) return a.count > b.count end)

    return tags
end

-- Cache for unique tags (rebuilt when items change)
local cachedTags = nil

-- Column widths for 3-column layout
-- Right-align status columns against scrollbar, maximize name space
local COL_STATUS_SIZE = 14       -- Icon size for status columns
local COL_STATUS_PADDING = 2     -- Padding between status columns (tight)
local COL_RIGHT_MARGIN = 2       -- Right margin from scrollbar (minimal)

-- Create or reuse item list entry row (with pooling)
-- 3-column layout: | Icon + Name | Collected | Craftable |
local function CreateItemListEntry(parent, width, yOffset, itemData, index)
    local row = rowPool:Acquire(parent)

    -- Update row size and position
    row:SetSize(width, CONFIG.PREVIEW_TAB.ITEM_ROW_HEIGHT)
    row:SetPoint("TOPLEFT", 0, yOffset)

    -- Update row data
    row.rowIndex = index
    row.itemData = itemData

    -- Update base color based on index
    local baseColor
    local rowScheme = GetScheme()
    if index % 2 == 0 then
        if rowScheme then
            baseColor = {rowScheme.panel.r, rowScheme.panel.g, rowScheme.panel.b, 0.6}
        else
            baseColor = {0.1, 0.1, 0.12, 0.6}
        end
    else
        if rowScheme then
            baseColor = {rowScheme.bg.r, rowScheme.bg.g, rowScheme.bg.b, 0.6}
        else
            baseColor = {0.08, 0.08, 0.1, 0.6}
        end
    end
    row.baseColor = baseColor

    -- Update selection state
    local isSelected = (selectedDecorID == itemData.decorID)
    if isSelected then
        if rowScheme then
            row:SetBackdropColor(rowScheme.accent.r, rowScheme.accent.g, rowScheme.accent.b, 0.8)
        else
            row:SetBackdropColor(0.2, 0.4, 0.6, 0.8)
        end
    else
        row:SetBackdropColor(unpack(baseColor))
    end

    -- Update item icon
    if itemData.itemID then
        local _, _, _, itemIcon = GetCachedItemInfo(itemData.itemID)
        if itemIcon then
            row.icon:SetTexture(itemIcon)
        else
            row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
            C_Item.RequestLoadItemDataByID(itemData.itemID)
        end
    elseif itemData.iconTexture then
        row.icon:SetTexture(itemData.iconTexture)
    else
        row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
    end

    -- Column 1: Item name (with quality color if available)
    -- Use localized name from WoW API when itemID is available
    local itemName = itemData.itemID and HDG_Data.GetLocalizedName(itemData.itemID, itemData.name) or itemData.name or "Unknown"
    if itemData.itemID then
        local _, _, quality = GetCachedItemInfo(itemData.itemID)
        if quality then
            local r, g, b = GetQualityColor(quality)
            row.nameText:SetText(string.format("|cFF%02x%02x%02x%s|r", r*255, g*255, b*255, itemName))
        else
            row.nameText:SetText(HDG.Theme.C_Text() .. itemName .. "|r")
        end
    else
        row.nameText:SetText(HDG.Theme.C_Text() .. itemName .. "|r")
    end

    -- Column 2: Collected status (checkmark icon, hidden if not collected)
    local collected = HDG_Data.IsDecorCollected(itemData.decorID)
    if collected then
        row.collectedIcon:SetTexture(CONFIG.TEXTURES.CHECKMARK)
        row.collectedIcon:SetDesaturated(false)
        local colScheme = GetScheme()
        if colScheme then
            row.collectedIcon:SetVertexColor(colScheme.success.r, colScheme.success.g, colScheme.success.b)
        else
            row.collectedIcon:SetVertexColor(0.2, 0.9, 0.2)  -- Green
        end
        row.collectedIcon:SetAlpha(1)
        row.collectedIcon:Show()
    else
        row.collectedIcon:Hide()
    end

    -- Column 3: Craftable status
    -- Check if this item is craftable (has a spellID in our database)
    local isCraftable = itemData.spellID ~= nil
    if isCraftable then
        -- Check if the player knows the recipe
        local isKnown = HDG_Data and HDG_Data.PlayerKnowsRecipeBySpellID and HDG_Data.PlayerKnowsRecipeBySpellID(itemData.spellID)

        -- Check if an alt knows the recipe (only if current char doesn't know it)
        local isAltKnown = false
        if not isKnown and HDG_Data.GetCharactersWhoKnowRecipe then
            local altsWhoKnow = HDG_Data.GetCharactersWhoKnowRecipe(itemData.spellID, itemData.profession)
            isAltKnown = altsWhoKnow and #altsWhoKnow > 0
        end

        local craftScheme = GetScheme()
        if isKnown then
            -- Gold star for known recipes (distinct from green checkmark)
            row.craftableIcon:SetTexture("Interface\\COMMON\\ReputationStar")
            row.craftableIcon:SetTexCoord(0, 0.5, 0, 0.5)  -- Use filled star quadrant
            row.craftableIcon:SetDesaturated(false)
            if craftScheme then
                row.craftableIcon:SetVertexColor(craftScheme.warning.r, craftScheme.warning.g, craftScheme.warning.b)
            else
                row.craftableIcon:SetVertexColor(1, 0.85, 0.1)  -- Bright gold
            end
            row.craftableIcon:SetAlpha(1)
        elseif isAltKnown then
            -- Grey star for recipes known by alts but not current character
            row.craftableIcon:SetTexture("Interface\\COMMON\\ReputationStar")
            row.craftableIcon:SetTexCoord(0, 0.5, 0, 0.5)  -- Use filled star quadrant
            row.craftableIcon:SetDesaturated(false)
            if craftScheme then
                local altColor = craftScheme.isLight and craftScheme.text or craftScheme.text_dim
                row.craftableIcon:SetVertexColor(altColor.r, altColor.g, altColor.b)
            else
                row.craftableIcon:SetVertexColor(0.6, 0.6, 0.6)  -- Grey
            end
            row.craftableIcon:SetAlpha(0.8)
        else
            -- Gray checkmark for craftable but not known by anyone
            row.craftableIcon:SetTexture(CONFIG.TEXTURES.CHECKMARK)
            row.craftableIcon:SetTexCoord(0, 1, 0, 1)
            row.craftableIcon:SetDesaturated(true)
            if craftScheme then
                row.craftableIcon:SetVertexColor(craftScheme.text.r, craftScheme.text.g, craftScheme.text.b)
            else
                row.craftableIcon:SetVertexColor(1, 1, 1)
            end
            row.craftableIcon:SetAlpha(0.5)
        end
    else
        -- X mark for not craftable
        row.craftableIcon:SetTexture(CONFIG.TEXTURES.XMARK)
        row.craftableIcon:SetTexCoord(0, 1, 0, 1)
        row.craftableIcon:SetDesaturated(false)
        local ncScheme = GetScheme()
        if ncScheme then
            local ncColor = ncScheme.isLight and ncScheme.text or ncScheme.text_dim
            row.craftableIcon:SetVertexColor(ncColor.r, ncColor.g, ncColor.b)
        else
            row.craftableIcon:SetVertexColor(0.4, 0.4, 0.4)
        end
        row.craftableIcon:SetAlpha(0.4)
    end

    row:Show()
    return yOffset - CONFIG.PREVIEW_TAB.ITEM_ROW_SPACING
end

local function UpdateModelDisplay(itemData)
    if not previewPanel or not previewPanel.decorPreview or not itemData then return end
    previewPanel.selectedItemData = itemData
    previewPanel.decorPreview:LoadItemData(itemData)
end

-- Update the info pane with all available data (two columns)
local function UpdateInfoPane(infoFrame, itemData)
    if not infoFrame or not itemData then return end
    
    -- Clear existing font strings
    if infoFrame.labels then
        for _, label in ipairs(infoFrame.labels) do
            label:Hide()
            label:SetText("")
        end
    end
    infoFrame.labels = {}

    -- Clear stale dye swatches from previous selection
    if infoFrame.dyeSwatches then
        for _, sw in ipairs(infoFrame.dyeSwatches) do sw:Hide() end
    end

    local lineHeight = 14
    local leftMargin = 8
    local midPoint = infoFrame:GetWidth() / 2 + 10
    
    local leftY = -8
    local rightY = -8
    
    local function AddLeftLine(text)
        local line = infoFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        line:SetPoint("TOPLEFT", leftMargin, leftY)
        line:SetWidth(midPoint - leftMargin - 10)
        line:SetJustifyH("LEFT")
        line:SetShadowOffset(0, 0)  -- Remove text shadow
        line:SetText(text)
        line:Show()
        table.insert(infoFrame.labels, line)
        leftY = leftY - lineHeight
        return line
    end

    local function AddRightLine(text)
        local line = infoFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        line:SetPoint("TOPLEFT", midPoint, rightY)
        line:SetPoint("RIGHT", -8, 0)
        line:SetJustifyH("LEFT")
        line:SetShadowOffset(0, 0)  -- Remove text shadow
        line:SetText(text)
        line:Show()
        table.insert(infoFrame.labels, line)
        rightY = rightY - lineHeight
        return line
    end
    
    -- Get fresh decorInfo from API - try by itemID first, fall back to recordID
    local decorInfo = nil
    if itemData.itemID and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
        decorInfo = C_HousingCatalog.GetCatalogEntryInfoByItem(itemData.itemID, true)
    end
    if not decorInfo and itemData.decorID and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfoByRecordID, 1, itemData.decorID, true)
        if ok and info then
            decorInfo = info
        end
    end
    
    -- === LEFT COLUMN: Player-interesting info ===

    -- Item name with quality color (if available)
    local r, g, b = 1, 1, 1  -- Default white
    local localizedDetailName = itemData.itemID and HDG_Data.GetLocalizedName(itemData.itemID, itemData.name) or itemData.name or "Unknown"
    if itemData.itemID then
        local _, _, quality = GetCachedItemInfo(itemData.itemID)
        if quality then
            r, g, b = GetQualityColor(quality)
        end
    end
    AddLeftLine(string.format("|cFF%02x%02x%02x%s|r", r*255, g*255, b*255, localizedDetailName))
    
    -- Category > Subcategory
    local catText = itemData.categoryName or "Uncategorized"
    if itemData.subcategoryName then
        catText = catText .. " > " .. itemData.subcategoryName
    end
    AddLeftLine(HDG.Theme.C_Accent() .. catText .. "|r")

    -- Profession (blue label, orange value)
    AddLeftLine(HDG.Theme.C_Accent() .. "Profession:|r " .. HDG.Theme.C_RecipeKnown() .. (itemData.profession or "Unknown") .. "|r")

    if decorInfo then
        -- Owned status - use firstAcquisitionBonus == 0 method (same as Recipes pane)
        local isCollected = HDG_Data.IsDecorCollected(itemData.decorID)
        if isCollected then
            local ownedText = HDG.Theme.C_Success() .. "Collected|r"
            if decorInfo.numStored and decorInfo.numStored > 0 then
                ownedText = ownedText .. " " .. HDG.Theme.C_Dim() .. "(Stored: " .. decorInfo.numStored .. ")|r"
            end
            if decorInfo.numPlaced and decorInfo.numPlaced > 0 then
                ownedText = ownedText .. " " .. HDG.Theme.C_Dim() .. "(Placed: " .. decorInfo.numPlaced .. ")|r"
            end
            AddLeftLine(ownedText)
        else
            AddLeftLine(HDG.Theme.C_Warning() .. "Not Collected|r")
        end

        -- Placement allowed (blue label, orange/dim values based on state)
        local allowedText = HDG.Theme.C_Accent() .. "Allowed:|r "
        if decorInfo.isAllowedIndoors then
            allowedText = allowedText .. HDG.Theme.C_RecipeKnown() .. "Indoors|r "
        else
            allowedText = allowedText .. HDG.Theme.C_Dim() .. "Indoors|r "
        end
        if decorInfo.isAllowedOutdoors then
            allowedText = allowedText .. HDG.Theme.C_RecipeKnown() .. "Outdoors|r"
        else
            allowedText = allowedText .. HDG.Theme.C_Dim() .. "Outdoors|r"
        end
        AddLeftLine(allowedText)

        -- Tags (blue label, orange values)
        if decorInfo.dataTagsByID then
            local tags = {}
            for _, tagName in pairs(decorInfo.dataTagsByID) do
                table.insert(tags, tagName)
            end
            if #tags > 0 then
                AddLeftLine(HDG.Theme.C_Accent() .. "Tags:|r " .. HDG.Theme.C_RecipeKnown() .. table.concat(tags, ", ") .. "|r")
            end
        end

        -- Dyeable status (left column)
        if decorInfo.canCustomize then
            AddLeftLine(HDG.Theme.C_Accent() .. "Dyeable:|r " .. HDG.Theme.C_Success() .. "Yes|r")

            -- Color swatch strip (one per dye category, uses pre-computed caches)
            pcall(function()
                if not HDG_DyeData or not HDG_DyeData.EnsureInitialized() then return end
                if not infoFrame.dyeSwatches then infoFrame.dyeSwatches = {} end

                local categories = HDG_DyeData.GetCategories()
                local swatchSize = 14
                local swatchX = leftMargin
                local swatchY = leftY - 2

                for i, catID in ipairs(categories) do
                    local sw = infoFrame.dyeSwatches[i]
                    if not sw then
                        sw = infoFrame:CreateTexture(nil, "ARTWORK")
                        sw:SetTexture("Interface\\Buttons\\WHITE8x8")
                        infoFrame.dyeSwatches[i] = sw
                    end
                    sw:SetSize(swatchSize, swatchSize)
                    sw:SetPoint("TOPLEFT", swatchX, swatchY)

                    local c = HDG_DyeData.GetCategorySwatchColor(catID)
                    if c then
                        local hasOwned = HDG_DyeData.HasOwnedDyeInCategory(catID)
                        sw:SetVertexColor(c.r, c.g, c.b, hasOwned and 1 or 0.3)
                    end
                    sw:Show()
                    swatchX = swatchX + swatchSize + 2
                end

                leftY = swatchY - swatchSize - 4
            end)
        end
    end

    -- === RIGHT COLUMN: Technical info ===

    -- IDs
    if itemData.itemID then
        AddRightLine(HDG.Theme.C_Dim() .. "Item ID:|r " .. HDG.Theme.C_Text() .. itemData.itemID .. "|r")
    end
    AddRightLine(HDG.Theme.C_Dim() .. "Decor ID:|r " .. HDG.Theme.C_Text() .. (itemData.decorID or "?") .. "|r")

    if decorInfo then
        -- Record ID
        if decorInfo.entryID and decorInfo.entryID.recordID then
            AddRightLine(HDG.Theme.C_Dim() .. "Record ID:|r " .. HDG.Theme.C_Text() .. decorInfo.entryID.recordID .. "|r")
        end

        -- Placement cost
        if decorInfo.placementCost then
            AddRightLine(HDG.Theme.C_Dim() .. "Place Cost:|r " .. HDG.Theme.C_Text() .. decorInfo.placementCost .. "|r")
        end

        -- Size (numeric)
        if decorInfo.size then
            AddRightLine(HDG.Theme.C_Dim() .. "Size Value:|r " .. HDG.Theme.C_Text() .. decorInfo.size .. "|r")
        end

        -- Asset ID
        if decorInfo.asset then
            AddRightLine(HDG.Theme.C_Dim() .. "Asset:|r " .. HDG.Theme.C_Dim() .. decorInfo.asset .. "|r")
        end


        -- Customizable (kept in right column as technical flag, full dye info in left)
        if decorInfo.canCustomize then
            AddRightLine(HDG.Theme.C_Dim() .. "Dyeable:|r " .. HDG.Theme.C_Success() .. "Yes|r")
        end
    end

    -- Expansion (from our DB)
    AddRightLine(HDG.Theme.C_Dim() .. "Expansion:|r " .. HDG.Theme.C_Text() .. (itemData.expansion or "?") .. "|r")

    -- (Dyes button removed — Dye Studio accessible from main title bar)

    -- "Find Vendor" button — show for non-crafted items that exist in VendorDB
    if infoFrame.findVendorBtn then
        infoFrame.findVendorBtn:Hide()
    end
    if itemData.itemID and HDG_VendorByItem and HDG_VendorByItem[itemData.itemID] then
        if not infoFrame.findVendorBtn then
            infoFrame.findVendorBtn = HDG.UI:Button(infoFrame, "Find Vendor", 90, 20, function()
                if infoFrame._findVendorItemID and HDG_UI and HDG_UI.SwitchTab then
                    HDG_UI.SwitchTab("acquisition")
                    if HDG_AcquisitionTab then
                        if not HDG_AcquisitionTab.initialized then
                            HDG_AcquisitionTab:Init(CONFIG, mainFrame, addon)
                            HDG_AcquisitionTab.initialized = true
                        end
                        HDG_AcquisitionTab:Show()
                        HDG_AcquisitionTab:SelectItem(infoFrame._findVendorItemID)
                    end
                end
            end)
        end
        infoFrame._findVendorItemID = itemData.itemID
        infoFrame.findVendorBtn:SetPoint("BOTTOMRIGHT", infoFrame, "BOTTOMRIGHT", -8, 6)
        infoFrame.findVendorBtn:Show()
    end
end

-- Initialize the module
function HDG_DecorPreview:Init(configRef, mainFrameRef, dbRef)
    CONFIG = configRef
    mainFrame = mainFrameRef
    db = dbRef
end

-- Create the preview panel
function HDG_DecorPreview:CreatePanel()
    if previewPanel then return previewPanel end

    -- Use factory for main panel (no header/scroll - custom layout with sub-panels)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    previewPanel = layout.frame
    previewPanel:Hide()

    -- Header background (matches Shopping List style)
    local headerBg = CreateFrame("Frame", nil, previewPanel, "BackdropTemplate")
    headerBg:SetPoint("TOPLEFT", 1, -1)
    headerBg:SetPoint("TOPRIGHT", -1, -1)
    headerBg:SetHeight(22)
    headerBg:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false })
    RegisterWidget(headerBg, "HeaderBar")  -- Theme-aware: updates on theme change

    -- Search box in header (left side)
    local searchBox = CreateFrame("EditBox", nil, headerBg, "SearchBoxTemplate")
    searchBox:SetSize(CONFIG.PREVIEW_TAB.SEARCH_BOX_WIDTH, CONFIG.PREVIEW_TAB.SEARCH_BOX_HEIGHT)
    searchBox:SetPoint("LEFT", 6, 0)
    searchBox:SetAutoFocus(false)
    searchBox:SetScript("OnTextChanged", function(self)
        SearchBoxTemplate_OnTextChanged(self)
        local searchText = self:GetText():lower()
        HDG_DecorPreview.searchFilter = searchText
        -- Debounce: wait 300ms after typing stops before refreshing
        if searchDebounceTimer then searchDebounceTimer:Cancel() end
        searchDebounceTimer = C_Timer.NewTimer(0.3, function()
            HDG_DecorPreview:RefreshList()
        end)
    end)
    RegisterWidget(searchBox, "SearchBox")  -- Theme-aware: updates on theme change
    previewPanel.searchBox = searchBox

    -- Hide Unreleased checkbox (default on — hides items not in AllDecorDB)
    HDG_DecorPreview.hideUnreleased = true
    local releasedCheck = HDG.UI:Checkbox(headerBg, 14, "Hide Unreleased")
    releasedCheck:SetPoint("LEFT", searchBox, "RIGHT", 6, 0)
    releasedCheck:SetChecked(true)
    releasedCheck.onChanged = function(checked)
        HDG_DecorPreview.hideUnreleased = checked
        knownDecorIDs = nil -- invalidate cache on toggle
        HDG_DecorPreview:RefreshList()
    end
    previewPanel.releasedCheck = releasedCheck

    local title = headerBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("CENTER", 0, 0)
    local titleScheme = GetScheme()
    ApplyFont(title, titleScheme, "header")
    title:SetText("Decor Preview")
    if titleScheme then
        title:SetTextColor(titleScheme.text_header.r, titleScheme.text_header.g, titleScheme.text_header.b, titleScheme.text_header.a)
    end
    title.isHeader = true
    HDG.Theme.StyleHeaderTitle(title, headerBg)
    previewPanel.title = title

    -- Filter bar background (themed for light/dark modes, registered for dynamic updates)
    local filterBarBg = CreateFrame("Frame", nil, previewPanel, "BackdropTemplate")
    filterBarBg:SetPoint("TOPLEFT", 1, -24)
    filterBarBg:SetPoint("TOPRIGHT", -1, -24)
    filterBarBg:SetHeight(40)  -- Covers both filter rows
    filterBarBg:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false })
    RegisterWidget(filterBarBg, "FilterBarBg")  -- Theme-aware: updates on theme change
    filterBarBg:SetFrameLevel(previewPanel:GetFrameLevel() + 1)
    previewPanel.filterBarBg = filterBarBg

    -- Row 1: Category buttons + All Decor button
    local categoryRow = CreateFrame("Frame", nil, previewPanel)
    categoryRow:SetPoint("TOPLEFT", 12, -28)
    categoryRow:SetPoint("RIGHT", previewPanel, "RIGHT", -12, 0)
    categoryRow:SetHeight(20)
    categoryRow:SetFrameLevel(filterBarBg:GetFrameLevel() + 1)
    previewPanel.categoryRow = categoryRow

    -- "Filter:" label before category buttons
    local filterLabel = categoryRow:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    filterLabel:SetPoint("LEFT", 0, 0)
    filterLabel:SetShadowOffset(0, 0)
    ApplyFont(filterLabel, titleScheme, "small")
    filterLabel:SetText(HDG.Theme.C_Label() .. "Filter:|r")
    RegisterWidget(filterLabel, "Text")
    previewPanel.filterLabel = filterLabel

    -- Segmented toggle container for Collection/Uncollected (stacked vertically)
    local toggleContainer = CreateFrame("Frame", nil, categoryRow, "BackdropTemplate")
    toggleContainer:SetSize(CONFIG.PREVIEW_TAB.TOGGLE_CONTAINER_WIDTH, CONFIG.PREVIEW_TAB.TOGGLE_CONTAINER_HEIGHT)
    toggleContainer:SetPoint("RIGHT", 0, -8)  -- Offset down to center between rows
    toggleContainer:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, edgeSize = 1,
        insets = {left = 0, right = 0, top = 0, bottom = 0}
    })
    local toggleScheme = GetScheme()
    if toggleScheme then
        toggleContainer:SetBackdropColor(toggleScheme.panel.r, toggleScheme.panel.g, toggleScheme.panel.b, 1.0)
        toggleContainer:SetBackdropBorderColor(toggleScheme.accent.r, toggleScheme.accent.g, toggleScheme.accent.b, 1)
        RegisterWidget(toggleContainer, "Panel")
    else
        toggleContainer:SetBackdropColor(0.08, 0.08, 0.1, 1.0)
        toggleContainer:SetBackdropBorderColor(0, 0.4, 0.4, 1)
    end
    previewPanel.toggleContainer = toggleContainer

    -- Top segment: My Collection
    local collectionBtn = CreateFrame("Button", nil, toggleContainer)
    collectionBtn:SetSize(CONFIG.PREVIEW_TAB.TOGGLE_CONTAINER_WIDTH - 2, CONFIG.PREVIEW_TAB.TOGGLE_BUTTON_HEIGHT)
    collectionBtn:SetPoint("TOP", 0, -1)

    local collectionBg = collectionBtn:CreateTexture(nil, "BACKGROUND")
    collectionBg:SetAllPoints()
    collectionBg:SetColorTexture(GetStateInactive())
    collectionBtn.bg = collectionBg

    local collectionText = collectionBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    collectionText:SetPoint("CENTER", 0, 0)
    ApplyFont(collectionText, titleScheme, "small")
    collectionText:SetText(HDG.Theme.C_Dim() .. "Collection|r")
    collectionBtn.text = collectionText

    -- Bottom segment: Uncollected
    local uncollectedBtn = CreateFrame("Button", nil, toggleContainer)
    uncollectedBtn:SetSize(CONFIG.PREVIEW_TAB.TOGGLE_CONTAINER_WIDTH - 2, CONFIG.PREVIEW_TAB.TOGGLE_BUTTON_HEIGHT)
    uncollectedBtn:SetPoint("BOTTOM", 0, 1)

    local uncollectedBg = uncollectedBtn:CreateTexture(nil, "BACKGROUND")
    uncollectedBg:SetAllPoints()
    uncollectedBg:SetColorTexture(GetStateInactive())
    uncollectedBtn.bg = uncollectedBg

    local uncollectedLabel = L["STATUS_UNCOLLECTED"] or "Uncollected"
    local uncollectedText = uncollectedBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    uncollectedText:SetPoint("CENTER", 0, 0)
    ApplyFont(uncollectedText, titleScheme, "small")
    uncollectedText:SetText(HDG.Theme.C_Dim() .. uncollectedLabel .. "|r")
    uncollectedBtn.text = uncollectedText

    -- Update both segments based on state
    local collectionLabel = L["STATUS_COLLECTION"] or "Collection"
    local function UpdateCollectionToggle()
        if HDG_DecorPreview.showAllDecor then
            -- Collection active
            collectionBg:SetColorTexture(GetStateActive())
            collectionText:SetText(HDG.Theme.C_Text() .. collectionLabel .. "|r")
        else
            -- Collection inactive
            collectionBg:SetColorTexture(GetStateInactive())
            collectionText:SetText(HDG.Theme.C_Dim() .. collectionLabel .. "|r")
        end

        if HDG_DecorPreview.uncollectedOnly then
            -- Uncollected active
            uncollectedBg:SetColorTexture(GetStateActive())
            uncollectedText:SetText(HDG.Theme.C_Text() .. uncollectedLabel .. "|r")
        else
            -- Uncollected inactive
            uncollectedBg:SetColorTexture(GetStateInactive())
            uncollectedText:SetText(HDG.Theme.C_Dim() .. uncollectedLabel .. "|r")
        end
    end

    collectionBtn:SetScript("OnClick", function()
        HDG_DecorPreview.showAllDecor = not HDG_DecorPreview.showAllDecor
        UpdateCollectionToggle()
        HDG_DecorPreview:RefreshList()
        HDG_DecorPreview:RefreshTagFilters()
    end)
    collectionBtn:SetScript("OnEnter", function(self)
        if not HDG_DecorPreview.showAllDecor then
            self.bg:SetColorTexture(GetStateHover())
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(L["TIP_MY_COLLECTION"] or "My Collection", 1, 0.82, 0.3)
        GameTooltip:AddLine(L["TIP_SHOW_ALL_DECOR"] or "Show ALL housing decor items from the catalog", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(L["TIP_NOT_JUST_CRAFTABLE"] or "(not just craftable recipes).", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    collectionBtn:SetScript("OnLeave", function(self)
        UpdateCollectionToggle()
        GameTooltip:Hide()
    end)
    previewPanel.collectionBtn = collectionBtn

    uncollectedBtn:SetScript("OnClick", function()
        HDG_DecorPreview.uncollectedOnly = not HDG_DecorPreview.uncollectedOnly
        UpdateCollectionToggle()
        HDG_DecorPreview:RefreshList()
    end)
    uncollectedBtn:SetScript("OnEnter", function(self)
        if not HDG_DecorPreview.uncollectedOnly then
            self.bg:SetColorTexture(GetStateHover())
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(L["TIP_UNCOLLECTED_ONLY"] or "Uncollected Only", 1, 0.82, 0.3)
        GameTooltip:AddLine(L["TIP_HIDE_COLLECTED"] or "Hide items you've already collected", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    uncollectedBtn:SetScript("OnLeave", function(self)
        UpdateCollectionToggle()
        GameTooltip:Hide()
    end)
    previewPanel.uncollectedBtn = uncollectedBtn
    UpdateCollectionToggle()

    -- Row 2: Tag buttons for selected category
    local tagRow = CreateFrame("Frame", nil, previewPanel)
    tagRow:SetPoint("TOPLEFT", categoryRow, "BOTTOMLEFT", 0, -2)
    tagRow:SetPoint("RIGHT", previewPanel, "RIGHT", -12, 0)
    tagRow:SetHeight(18)
    previewPanel.tagRow = tagRow

    -- "Tags:" label before tag buttons
    local tagsLabel = tagRow:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    tagsLabel:SetPoint("LEFT", 0, 0)
    tagsLabel:SetShadowOffset(0, 0)
    ApplyFont(tagsLabel, titleScheme, "small")
    tagsLabel:SetText(HDG.Theme.C_Label() .. "Tags:|r")
    previewPanel.tagsLabel = tagsLabel

    -- Containers for buttons
    previewPanel.categoryButtons = {}
    previewPanel.tagButtons = {}

    -- Initialize tag filter state
    HDG_DecorPreview.tagFilters = HDG_DecorPreview.tagFilters or {}
    HDG_DecorPreview.selectedCategory = HDG_DecorPreview.selectedCategory or "All"
    if HDG_DecorPreview.showAllDecor == nil then HDG_DecorPreview.showAllDecor = true end

    -- Left panel: Item list (starts below filter rows)
    local listPanel = CreateFrame("Frame", nil, previewPanel, "BackdropTemplate")
    listPanel:SetPoint("TOPLEFT", 8, -70)
    listPanel:SetPoint("BOTTOMLEFT", 8, 8)
    listPanel:SetWidth(CONFIG.PREVIEW_TAB.LIST_PANEL_WIDTH)
    listPanel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        tile = false
    })
    local listScheme = GetScheme()
    if listScheme then
        listPanel:SetBackdropColor(listScheme.bg.r, listScheme.bg.g, listScheme.bg.b, listScheme.bg.a)
        RegisterWidget(listPanel, "Background")
    else
        listPanel:SetBackdropColor(0.08, 0.08, 0.1, 0.95)
    end

    -- Column headers row (3 columns: Name, Collected, Craftable)
    local headerRow = CreateFrame("Frame", nil, listPanel, "BackdropTemplate")
    headerRow:SetPoint("TOPLEFT", 1, -1)
    headerRow:SetPoint("TOPRIGHT", -1, -1)
    headerRow:SetHeight(18)
    headerRow:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false })
    RegisterWidget(headerRow, "PanelHeader")  -- Theme-aware: uses panel_header color

    -- Header: Title with count - matches "3D Preview" styling
    local headerTitle = headerRow:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerTitle:SetPoint("LEFT", 6, 0)
    headerTitle:SetShadowOffset(0, 0)
    ApplyFont(headerTitle, listScheme, "small")
    headerTitle:SetText(HDG.Theme.C_Text() .. "Items|r")
    previewPanel.headerTitle = headerTitle

    -- Header icons align with list content (account for scrollbar width + scroll padding)
    local HEADER_SCROLLBAR_OFFSET = 23  -- listScroll has -24 right offset, plus 1px header inset

    -- Header: Craftable icon (rightmost) - with tooltip
    local headerCraftableFrame = CreateFrame("Frame", nil, headerRow)
    headerCraftableFrame:SetSize(COL_STATUS_SIZE, COL_STATUS_SIZE)
    headerCraftableFrame:SetPoint("RIGHT", -(COL_RIGHT_MARGIN + HEADER_SCROLLBAR_OFFSET), 0)
    headerCraftableFrame:EnableMouse(true)
    local headerCraftableIcon = headerCraftableFrame:CreateTexture(nil, "ARTWORK")
    headerCraftableIcon:SetAllPoints()
    headerCraftableIcon:SetTexture("Interface\\Icons\\INV_Misc_Book_09")
    previewPanel.headerCraftableIcon = headerCraftableIcon  -- Store for theme refresh
    headerCraftableFrame:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:AddLine("Craftable", 1, 0.82, 0.3)
        GameTooltip:AddLine("Recipe is available to craft", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Bright Star|r = Known by you", 0.6, 0.6, 0.6)
        GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Dim Star|r = Known by alt", 0.6, 0.6, 0.6)
        GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Dim Check|r = Not known", 0.6, 0.6, 0.6)
        GameTooltip:Show()
    end)
    headerCraftableFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Header: Collected icon - to the left of craftable, with tooltip
    local headerCollectedFrame = CreateFrame("Frame", nil, headerRow)
    headerCollectedFrame:SetSize(COL_STATUS_SIZE, COL_STATUS_SIZE)
    headerCollectedFrame:SetPoint("RIGHT", headerCraftableFrame, "LEFT", -COL_STATUS_PADDING, 0)
    headerCollectedFrame:EnableMouse(true)
    local headerCollectedIcon = headerCollectedFrame:CreateTexture(nil, "ARTWORK")
    headerCollectedIcon:SetAllPoints()
    headerCollectedIcon:SetTexture("Interface\\Icons\\INV_Crate_04")
    previewPanel.headerCollectedIcon = headerCollectedIcon  -- Store for theme refresh
    headerCollectedFrame:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:AddLine("Collected", 1, 0.82, 0.3)
        GameTooltip:AddLine("Decor item is in your collection", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(HDG.Theme.C_Success() .. "Green|r = Collected", 0.6, 0.6, 0.6)
        GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Gray|r = Not collected", 0.6, 0.6, 0.6)
        GameTooltip:Show()
    end)
    headerCollectedFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Item list scroll frame (starts below header row)
    local listScroll = CreateFrame("ScrollFrame", nil, listPanel, "UIPanelScrollFrameTemplate")
    listScroll:SetPoint("TOPLEFT", 6, -20)  -- Below 18px header + 2px padding
    listScroll:SetPoint("BOTTOMRIGHT", -24, 28)  -- Leave room for Add All button

    local listContent = CreateFrame("Frame", nil, listScroll)
    listContent:SetSize(CONFIG.PREVIEW_TAB.LIST_CONTENT_WIDTH, 1)
    listScroll:SetScrollChild(listContent)
    previewPanel.listContent = listContent
    previewPanel.listScroll = listScroll

    -- Initialize row pool
    rowPool = HDG_UIHelpers.CreateRowPool(function(parent)
        local row = CreateFrame("Button", nil, parent, "BackdropTemplate")
        row:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
        })

        -- Column 1: Item icon
        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(CONFIG.PREVIEW_TAB.ITEM_ICON_SIZE, CONFIG.PREVIEW_TAB.ITEM_ICON_SIZE)
        row.icon:SetPoint("LEFT", 4, 0)

        -- Column 1: Item name (after icon) - stretches to fill available space
        row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row.nameText:SetPoint("LEFT", 26, 0)
        row.nameText:SetPoint("RIGHT", -36, 0)
        row.nameText:SetJustifyH("LEFT")
        row.nameText:SetWordWrap(false)
        row.nameText:SetShadowOffset(0, 0)
        ApplyFont(row.nameText, GetScheme(), "small")

        -- Column 3: Craftable status icon (rightmost) - anchor from RIGHT
        row.craftableIcon = row:CreateTexture(nil, "ARTWORK")
        row.craftableIcon:SetSize(COL_STATUS_SIZE, COL_STATUS_SIZE)
        row.craftableIcon:SetPoint("RIGHT", -COL_RIGHT_MARGIN, 0)

        -- Column 2: Collected status icon - to the left of craftable
        row.collectedIcon = row:CreateTexture(nil, "ARTWORK")
        row.collectedIcon:SetSize(COL_STATUS_SIZE, COL_STATUS_SIZE)
        row.collectedIcon:SetPoint("RIGHT", row.craftableIcon, "LEFT", -COL_STATUS_PADDING, 0)

        -- Click handler (set once, uses self.itemData which is updated per-row)
        row:SetScript("OnClick", function(self, button)
            if IsShiftKeyDown() and self.itemData and HDG.Store then
                local itemID = self.itemData.itemID
                if itemID then
                    local localizedName = HDG_Data.GetLocalizedName(itemID, self.itemData.name) or self.itemData.name or "item"
                    local npcIDs = HDG_VendorByItem and HDG_VendorByItem[itemID]
                    local npcID = (npcIDs and #npcIDs > 0) and npcIDs[1] or nil
                    HDG.Store:AddToVendorShoppingList(itemID, npcID)
                    local dest = npcID and "shopping list" or "wish list"
                    print(HDG.Theme.C_Success() .. "[HDG]|r Added " .. "|cFF14b8a6" .. localizedName .. "|r to " .. dest)
                    if HDG_VendorShoppingList and HDG_VendorShoppingList.IsShown and HDG_VendorShoppingList.IsShown() then
                        HDG_VendorShoppingList.Refresh()
                    end
                    if HDG_UI and HDG_UI.UpdateShopListBadge then HDG_UI.UpdateShopListBadge() end
                else
                    print(HDG.Theme.C_Warning() .. "[HDG]|r Could not find item ID for this decor")
                end
            else
                local clickedItem = self.itemData -- capture before Refresh() recycles pool
                selectedDecorID = clickedItem.decorID
                HDG_DecorPreview:Refresh()
                HDG_DecorPreview:UpdateModelAndInfo(clickedItem)
            end
        end)

        -- Hover effects
        row:SetScript("OnEnter", function(self)
            if selectedDecorID ~= self.itemData.decorID then
                local hoverScheme = GetScheme()
                if hoverScheme then
                    self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, 0.8)
                else
                    self:SetBackdropColor(0.15, 0.15, 0.2, 0.8)
                end
            end
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)

            local function AddCustomLines()
                GameTooltip:AddLine(" ")
                local isCollected = HDG_Data.IsDecorCollected(self.itemData.decorID)
                if isCollected then
                    GameTooltip:AddLine(HDG.Theme.C_Success() .. "Item collected|r", 0.2, 0.9, 0.2)
                else
                    GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Not collected|r", 0.78, 0.78, 0.78)
                end
                if self.itemData.spellID then
                    local isKnown = HDG_Data and HDG_Data.PlayerKnowsRecipeBySpellID and HDG_Data.PlayerKnowsRecipeBySpellID(self.itemData.spellID)
                    if isKnown then
                        local charName = UnitName("player")
                        GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Recipe known by:|r " .. charName, 1, 0.85, 0.1)
                    else
                        local profession = self.itemData.profession or "Unknown"
                        GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Craftable by:|r " .. profession, 0.78, 0.78, 0.78)
                    end
                end
                if self.itemData.itemID and HDG_VendorResolver then
                    local vendors = HDG_VendorResolver.GetVendorsForItem(self.itemData.itemID)
                    if vendors and #vendors > 0 then
                        GameTooltip:AddLine(" ")
                        GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Available from vendor:|r", 0.9, 0.7, 0.2)
                        for i, vendor in ipairs(vendors) do
                            if i > 3 then
                                GameTooltip:AddLine(HDG.Theme.C_Dim() .. string.format("  ...and %d more", #vendors - 3) .. "|r", 0.78, 0.78, 0.78)
                                break
                            end
                            local priceStr = HDG_VendorResolver.FormatGoldCost(vendor.goldCost)
                            local vendorLine = "  " .. vendor.name .. " (" .. vendor.zone .. ")"
                            if priceStr then
                                vendorLine = vendorLine .. " - " .. priceStr
                            else
                                vendorLine = vendorLine .. " - |cFFAAAAAAcurrency|r"
                            end
                            GameTooltip:AddLine(vendorLine, 0.8, 0.8, 0.8)
                            if vendor.factionID and vendor.factionID ~= 0 then
                                local repStr = HDG_VendorResolver.FormatRepRequirement(vendor)
                                if repStr then
                                    GameTooltip:AddLine("    " .. repStr, 0.6, 0.8, 1.0)
                                end
                            end
                            if vendor.questID and vendor.questID ~= 0 then
                                local questStr = HDG_VendorResolver.FormatQuestRequirement(vendor)
                                if questStr then
                                    GameTooltip:AddLine("    Quest: " .. questStr, 0.6, 0.8, 1.0)
                                end
                            end
                            if vendor.achieveID and vendor.achieveID ~= 0 then
                                local achieveStr = HDG_VendorResolver.FormatAchievementRequirement(vendor)
                                if achieveStr then
                                    GameTooltip:AddLine("    Achievement: " .. achieveStr, 0.6, 0.8, 1.0)
                                end
                            end
                        end
                    end
                end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine(HDG.Theme.C_Success() .. "Shift-Click|r to add to Shopping List", 0.7, 0.7, 0.7)
                GameTooltip:Show()
            end

            if self.itemData.itemID then
                local item = Item:CreateFromItemID(self.itemData.itemID)
                item:ContinueOnItemLoad(function()
                    if GameTooltip:IsOwned(self) then
                        GameTooltip:SetItemByID(self.itemData.itemID)
                        AddCustomLines()
                    end
                end)
            else
                local localizedTooltipName = self.itemData.itemID and HDG_Data.GetLocalizedName(self.itemData.itemID, self.itemData.name) or self.itemData.name or "Unknown"
                GameTooltip:AddLine(localizedTooltipName, 1, 1, 1)
                GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Collected decor item|r", 0.78, 0.78, 0.78)
                if self.itemData.tags and #self.itemData.tags > 0 then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine("Tags: " .. table.concat(self.itemData.tags, ", "), 0.6, 0.6, 0.8)
                end
                GameTooltip:Show()
            end
        end)

        row:SetScript("OnLeave", function(self)
            if selectedDecorID ~= self.itemData.decorID then
                self:SetBackdropColor(unpack(self.baseColor))
            end
            GameTooltip:Hide()
        end)

        return row
    end)

    -- Apply minimalist scrollbar styling
    if HDG_UIHelpers and HDG_UIHelpers.StyleScrollBar then
        HDG_UIHelpers.StyleScrollBar(listScroll)
    end

    -- Add All to Queue button (at bottom of list panel)
    local addAllBtn = CreateFrame("Button", nil, listPanel, "BackdropTemplate")
    addAllBtn:SetSize(CONFIG.PREVIEW_TAB.LIST_PANEL_WIDTH - 16, 20)
    addAllBtn:SetPoint("BOTTOM", 0, 4)
    addAllBtn:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, edgeSize = 1,
    })
    local addBtnScheme = GetScheme()
    if addBtnScheme then
        addAllBtn:SetBackdropColor(addBtnScheme.button_normal.r, addBtnScheme.button_normal.g, addBtnScheme.button_normal.b, 0.9)
        addAllBtn:SetBackdropBorderColor(addBtnScheme.accent.r, addBtnScheme.accent.g, addBtnScheme.accent.b, 0.8)
        RegisterWidget(addAllBtn, "Button")
    else
        addAllBtn:SetBackdropColor(0.08, 0.35, 0.35, 0.9)
        addAllBtn:SetBackdropBorderColor(0, 0.5, 0.5, 0.8)
    end

    local addAllText = addAllBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    addAllText:SetPoint("CENTER", 0, 0)
    ApplyFont(addAllText, addBtnScheme, "small")
    addAllText:SetText(HDG.Theme.C_Text() .. "Add All to Queue|r")

    addAllBtn:SetScript("OnClick", function()
        -- Add all currently FILTERED craftable items to the crafting queue
        local items = GetAllDecorItems()
        local addedCount = 0

        for _, itemData in ipairs(items) do
            -- Use shared filter helper (same as RefreshList)
            if ItemPassesFilters(itemData) and itemData.spellID and itemData.itemID and itemData.reagents then
                if HDG_UI and HDG_UI.AddToShoppingList then
                    -- Skip refresh during batch add (true = skipRefresh)
                    HDG_UI.AddToShoppingList(BuildRecipeForQueue(itemData), true)
                    addedCount = addedCount + 1
                end
            end
        end

        -- Refresh once after all items added
        if addedCount > 0 then
            if HDG_UI and HDG_UI.RefreshUI then
                HDG_UI.RefreshUI()
            end
            print(string.format(HDG.Theme.C_Accent() .. "[Housing Decor Guide]|r Added %d craftable item(s) to crafting queue", addedCount))
        else
            print(HDG.Theme.C_Accent() .. "[Housing Decor Guide]|r No craftable items to add (check your filters)")
        end
    end)

    addAllBtn:SetScript("OnEnter", function(self)
        local hScheme = GetScheme()
        if hScheme then
            self:SetBackdropColor(hScheme.button_hover.r, hScheme.button_hover.g, hScheme.button_hover.b, 1.0)
        else
            self:SetBackdropColor(0.12, 0.45, 0.45, 1.0)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Add All to Queue|r", 1, 1, 1)
        GameTooltip:AddLine("Adds all currently displayed craftable items", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("to your crafting queue", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)

    addAllBtn:SetScript("OnLeave", function(self)
        local lScheme = GetScheme()
        if lScheme then
            self:SetBackdropColor(lScheme.button_normal.r, lScheme.button_normal.g, lScheme.button_normal.b, 0.9)
        else
            self:SetBackdropColor(0.08, 0.35, 0.35, 0.9)
        end
        GameTooltip:Hide()
    end)

    previewPanel.addAllBtn = addAllBtn
    
    -- Right panel: Model + Info
    local rightPanel = CreateFrame("Frame", nil, previewPanel)
    rightPanel:SetPoint("TOPLEFT", listPanel, "TOPRIGHT", 8, 0)
    rightPanel:SetPoint("BOTTOMRIGHT", -8, 8)
    
    -- Model viewer (larger - info panel will be smaller)
    local modelPanel = CreateFrame("Frame", nil, rightPanel, "BackdropTemplate")
    modelPanel:SetPoint("TOPLEFT", 0, 0)
    modelPanel:SetPoint("TOPRIGHT", 0, 0)
    modelPanel:SetHeight(CONFIG.PREVIEW_TAB.MODEL_PANEL_HEIGHT)
    modelPanel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, edgeSize = 1,
        insets = {left = 0, right = 0, top = 0, bottom = 0}
    })
    local modelScheme = GetScheme()
    if modelScheme then
        modelPanel:SetBackdropColor(modelScheme.bg.r, modelScheme.bg.g, modelScheme.bg.b, 0.95)
        modelPanel:SetBackdropBorderColor(modelScheme.border.r, modelScheme.border.g, modelScheme.border.b, modelScheme.border.a)
        RegisterWidget(modelPanel, "Panel")
    else
        modelPanel:SetBackdropColor(0.05, 0.05, 0.08, 0.95)
        modelPanel:SetBackdropBorderColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    end

    local decorPreview = HDG_UIHelpers.CreateDecorPreview(modelPanel, {
        squareAspect = false,
        showOverlays = false,
        showControls = true,
        showCorbels = true,
        placeholderText = "Select an item to preview",
    })
    decorPreview:SetAllPoints()
    previewPanel.decorPreview = decorPreview
    
    -- Info panel (25% of right panel height)
    local infoPanel = CreateFrame("Frame", nil, rightPanel, "BackdropTemplate")
    infoPanel:SetPoint("TOPLEFT", modelPanel, "BOTTOMLEFT", 0, -8)
    infoPanel:SetPoint("BOTTOMRIGHT", 0, 0)
    infoPanel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, edgeSize = 1,
        insets = {left = 0, right = 0, top = 0, bottom = 0}
    })
    local infoScheme = GetScheme()
    if infoScheme then
        infoPanel:SetBackdropColor(infoScheme.panel.r, infoScheme.panel.g, infoScheme.panel.b, 0.9)
        infoPanel:SetBackdropBorderColor(infoScheme.border.r, infoScheme.border.g, infoScheme.border.b, infoScheme.border.a)
        RegisterWidget(infoPanel, "Panel")
    else
        infoPanel:SetBackdropColor(0.08, 0.08, 0.1, 0.9)
        infoPanel:SetBackdropBorderColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    end
    
    previewPanel.infoPanel = infoPanel
    
    mainFrame.previewPanel = previewPanel
    return previewPanel
end

-- Tag category definitions - map tags to categories (English canonical names)
local TAG_CATEGORIES = {
    Sizes = { "Small", "Medium", "Large", "Tiny", "Huge", "Lavish" },
    Factions = { "Alliance", "Horde", "Draenei", "Orcish", "Nightborne", "Gilnean / Worgen",
                 "Zandalari Troll", "Troll", "Human", "Kul Tiran", "Tauren", "Goblin", "Gnomish",
                 "Bronzebeard Dwarf (Ironforge)", "Dark Iron Dwarf", "Blood Elf", "Pandaren",
                 "Dwarven", "Elven", "Undead", "Earthen (Dornish)", "Dracthyr" },
    Styles = { "Elegant", "Bold", "Simple", "Rugged", "Spooky", "Pirate", "Fae", "Cute", "Cozy",
               "Light", "Magical", "Mechanical", "Nature" },
    Expansions = { "Classic", "The Burning Crusade", "Burning Crusade", "Wrath of the Lich King",
                   "Cataclysm", "Mists of Pandaria", "Warlords of Draenor", "Legion",
                   "Battle for Azeroth", "Shadowlands", "Dragonflight", "The War Within", "Midnight" },
}

-- Reverse lookup: Localized tag name -> English canonical name
-- This allows tags from non-English clients to be categorized correctly
local TAG_TO_ENGLISH = {
    -- Spanish (esES/esMX)
    ["Pequeño"] = "Small", ["Mediano"] = "Medium", ["Grande"] = "Large",
    ["Diminuto"] = "Tiny", ["Enorme"] = "Huge", ["Suntuoso"] = "Lavish",
    ["Alianza"] = "Alliance", ["Horda"] = "Horde", ["Draenei"] = "Draenei",
    ["Orcos"] = "Orcish", ["Natonocturno"] = "Nightborne", ["Gilneanos / Huargen"] = "Gilnean / Worgen",
    ["Gilneanos"] = "Gilnean / Worgen", ["Trol Zandalari"] = "Zandalari Troll", ["Trol"] = "Troll",
    ["Humano"] = "Human", ["Ciudadano de Kul Tiras"] = "Kul Tiran", ["Tauren"] = "Tauren",
    ["Goblin"] = "Goblin", ["Gnomo"] = "Gnomish", ["Enanos"] = "Dwarven",
    ["Enano Barbabronce (Forjaz)"] = "Bronzebeard Dwarf (Ironforge)", ["Enano Hierro Negro"] = "Dark Iron Dwarf",
    ["Elfo de Sangre"] = "Blood Elf", ["Pandaren"] = "Pandaren", ["Élfico"] = "Elven",
    ["No-muerto"] = "Undead", ["Terráneos (dórnicos)"] = "Earthen (Dornish)", ["Dracthyr"] = "Dracthyr",
    ["Elegante"] = "Elegant", ["Audaz"] = "Bold", ["Simple"] = "Simple", ["Rústico"] = "Rugged",
    ["Tétrico"] = "Spooky", ["Pirata"] = "Pirate", ["Feérico"] = "Fae", ["Adorable"] = "Cute",
    ["Acogedor"] = "Cozy", ["Luminoso"] = "Light", ["Mágica"] = "Magical", ["Mecánico"] = "Mechanical",
    ["Naturaleza"] = "Nature",
    -- French (frFR)
    ["Petit"] = "Small", ["Moyen"] = "Medium", ["Grand"] = "Large",
    ["Minuscule"] = "Tiny", ["Énorme"] = "Huge", ["Somptueux"] = "Lavish",
    ["Draeneï"] = "Draenei", ["Orc"] = "Orcish", ["Sacrenuit"] = "Nightborne",
    ["Gilnéen / Worgen"] = "Gilnean / Worgen", ["Troll zandalari"] = "Zandalari Troll",
    ["Humain"] = "Human", ["Kultirassien"] = "Kul Tiran", ["Gnome"] = "Gnomish",
    ["Nain de Forgefer"] = "Bronzebeard Dwarf (Ironforge)", ["Nain sombrefer"] = "Dark Iron Dwarf",
    ["Elfe de sang"] = "Blood Elf", ["Pandarène"] = "Pandaren", ["Nain"] = "Dwarven",
    ["Elfique"] = "Elven", ["Mort-vivant"] = "Undead", ["Terrestre (dornogal)"] = "Earthen (Dornish)",
    ["Élégant"] = "Elegant", ["Audacieux"] = "Bold", ["Rustique"] = "Rugged",
    ["Effrayant"] = "Spooky", ["Faë"] = "Fae", ["Mignon"] = "Cute",
    ["Douillet"] = "Cozy", ["Lumineux"] = "Light", ["Magique"] = "Magical", ["Mécanique"] = "Mechanical",
    ["Nature"] = "Nature",
    -- German (deDE)
    ["Klein"] = "Small", ["Mittel"] = "Medium", ["Groß"] = "Large",
    ["Winzig"] = "Tiny", ["Riesig"] = "Huge", ["Üppig"] = "Lavish",
    ["Allianz"] = "Alliance", ["Orkisch"] = "Orcish", ["Nachtgeborene"] = "Nightborne",
    ["Gilnäisch / Worgen"] = "Gilnean / Worgen", ["Zandalaritroll"] = "Zandalari Troll",
    ["Mensch"] = "Human", ["Kul Tiraner"] = "Kul Tiran", ["Gnomisch"] = "Gnomish",
    ["Bronzebartzwerg (Eisenschmiede)"] = "Bronzebeard Dwarf (Ironforge)", ["Dunkeleisenzwerg"] = "Dark Iron Dwarf",
    ["Blutelf"] = "Blood Elf", ["Zwergisch"] = "Dwarven", ["Elfisch"] = "Elven",
    ["Untot"] = "Undead", ["Irdene (Dornogal)"] = "Earthen (Dornish)",
    ["Kühn"] = "Bold", ["Robust"] = "Rugged", ["Gruselig"] = "Spooky",
    ["Feenhaft"] = "Fae", ["Niedlich"] = "Cute", ["Gemütlich"] = "Cozy",
    ["Hell"] = "Light", ["Magisch"] = "Magical", ["Mechanisch"] = "Mechanical", ["Natur"] = "Nature",
    -- Portuguese (ptBR)
    ["Pequeno"] = "Small", ["Médio"] = "Medium", ["Minúsculo"] = "Tiny",
    ["Gigante"] = "Huge", ["Luxuoso"] = "Lavish", ["Órquico"] = "Orcish",
    ["Anão Barbabronze (Altaforja)"] = "Bronzebeard Dwarf (Ironforge)", ["Anão Ferro Negro"] = "Dark Iron Dwarf",
    ["Elfo Sangrento"] = "Blood Elf", ["Anão"] = "Dwarven", ["Élfico"] = "Elven",
    ["Morto-vivo"] = "Undead", ["Terreno (Dornogal)"] = "Earthen (Dornish)",
    ["Ousado"] = "Bold", ["Rústico"] = "Rugged", ["Assustador"] = "Spooky",
    ["Feérico"] = "Fae", ["Fofo"] = "Cute", ["Aconchegante"] = "Cozy",
    ["Claro"] = "Light", ["Mágico"] = "Magical", ["Mecânico"] = "Mechanical", ["Natureza"] = "Nature",
    -- Italian (itIT)
    ["Piccolo"] = "Small", ["Medio"] = "Medium", ["Minuscolo"] = "Tiny",
    ["Immenso"] = "Huge", ["Sontuoso"] = "Lavish", ["Alleanza"] = "Alliance", ["Orda"] = "Horde",
    ["Orchesco"] = "Orcish", ["Nobile Oscuro"] = "Nightborne", ["Gilneano / Worgen"] = "Gilnean / Worgen",
    ["Troll Zandalari"] = "Zandalari Troll", ["Umano"] = "Human", ["Kul Tirano"] = "Kul Tiran",
    ["Gnomesco"] = "Gnomish", ["Nano Barbabronzea (Ironforge)"] = "Bronzebeard Dwarf (Ironforge)",
    ["Nano Ferroscuro"] = "Dark Iron Dwarf", ["Elfo del Sangue"] = "Blood Elf", ["Nanesco"] = "Dwarven",
    ["Elfico"] = "Elven", ["Non Morto"] = "Undead", ["Terrigeno (Dornogal)"] = "Earthen (Dornish)",
    ["Coraggioso"] = "Bold", ["Grezzo"] = "Rugged", ["Spaventoso"] = "Spooky",
    ["Fatato"] = "Fae", ["Carino"] = "Cute", ["Accogliente"] = "Cozy",
    ["Luminoso"] = "Light", ["Magico"] = "Magical", ["Meccanico"] = "Mechanical", ["Natura"] = "Nature",
    -- Russian (ruRU)
    ["Маленький"] = "Small", ["Средний"] = "Medium", ["Большой"] = "Large",
    ["Крошечный"] = "Tiny", ["Огромный"] = "Huge", ["Роскошный"] = "Lavish",
    ["Альянс"] = "Alliance", ["Орда"] = "Horde", ["Дренейский"] = "Draenei",
    ["Оркский"] = "Orcish", ["Ночнорожденный"] = "Nightborne", ["Воргенский"] = "Gilnean / Worgen",
    ["Зандаларский тролль"] = "Zandalari Troll", ["Тролль"] = "Troll", ["Человеческий"] = "Human",
    ["Кул-тирасский"] = "Kul Tiran", ["Таурен"] = "Tauren", ["Гоблинский"] = "Goblin",
    ["Гномский"] = "Gnomish", ["Дворф Бронзобород"] = "Bronzebeard Dwarf (Ironforge)",
    ["Дворф Черного Железа"] = "Dark Iron Dwarf", ["Эльф крови"] = "Blood Elf", ["Пандарен"] = "Pandaren",
    ["Дворфийский"] = "Dwarven", ["Эльфийский"] = "Elven", ["Нежить"] = "Undead",
    ["Земельник (Дорногал)"] = "Earthen (Dornish)", ["Драктир"] = "Dracthyr",
    ["Элегантный"] = "Elegant", ["Дерзкий"] = "Bold", ["Простой"] = "Simple", ["Грубый"] = "Rugged",
    ["Жуткий"] = "Spooky", ["Пиратский"] = "Pirate", ["Волшебный"] = "Fae", ["Милый"] = "Cute",
    ["Уютный"] = "Cozy", ["Светлый"] = "Light", ["Магический"] = "Magical", ["Механический"] = "Mechanical",
    ["Природный"] = "Nature",
    -- Korean (koKR)
    ["작은"] = "Small", ["중간"] = "Medium", ["큰"] = "Large",
    ["아주 작은"] = "Tiny", ["거대한"] = "Huge", ["호화로운"] = "Lavish",
    ["얼라이언스"] = "Alliance", ["호드"] = "Horde",
    -- Chinese Simplified (zhCN)
    ["小型"] = "Small", ["中型"] = "Medium", ["大型"] = "Large",
    ["微型"] = "Tiny", ["巨型"] = "Huge", ["奢华"] = "Lavish",
    ["联盟"] = "Alliance", ["部落"] = "Horde", ["德莱尼"] = "Draenei",
    ["兽人"] = "Orcish", ["夜之子"] = "Nightborne", ["吉尔尼斯/狼人"] = "Gilnean / Worgen",
    ["赞达拉巨魔"] = "Zandalari Troll", ["巨魔"] = "Troll", ["人类"] = "Human",
    ["库尔提拉斯"] = "Kul Tiran", ["牛头人"] = "Tauren", ["地精"] = "Goblin",
    ["侏儒"] = "Gnomish", ["铜须矮人(铁炉堡)"] = "Bronzebeard Dwarf (Ironforge)",
    ["黑铁矮人"] = "Dark Iron Dwarf", ["血精灵"] = "Blood Elf", ["熊猫人"] = "Pandaren",
    ["矮人"] = "Dwarven", ["精灵"] = "Elven", ["亡灵"] = "Undead",
    ["土灵(多恩诺加)"] = "Earthen (Dornish)", ["龙希尔"] = "Dracthyr",
    ["优雅"] = "Elegant", ["大胆"] = "Bold", ["简约"] = "Simple", ["粗犷"] = "Rugged",
    ["恐怖"] = "Spooky", ["海盗"] = "Pirate", ["仙灵"] = "Fae", ["可爱"] = "Cute",
    ["舒适"] = "Cozy", ["明亮"] = "Light", ["魔法"] = "Magical", ["机械"] = "Mechanical",
    ["自然"] = "Nature",
    -- Chinese Traditional (zhTW)
    ["小型"] = "Small", ["中型"] = "Medium", ["大型"] = "Large",
    ["微型"] = "Tiny", ["巨型"] = "Huge", ["奢華"] = "Lavish",
    ["聯盟"] = "Alliance", ["部落"] = "Horde", ["乾淨俐落"] = "Simple",
}

-- Short display names for expansion tags - use centralized HDG_ExpansionData
-- HDG_ExpansionData.GetShortName(tagName) returns 3-char short name (e.g., "TBC", "WLK")

-- Shortened names for faction tags (long names cause UI overlap)
local FACTION_TAG_SHORT = {
    ["Gilnean / Worgen"] = "Worgen",
    ["Pandaren"] = "Pandaren",
    ["Zandalari Troll"] = "Zandalari",
    ["Nightborne"] = "Nightborne",
    ["Kul Tiran"] = "Kul Tiran",
    ["Bronzebeard Dwarf (Ironforge)"] = "Bronzebeard",
    ["Dark Iron Dwarf"] = "Dark Iron",
    ["Earthen (Dornish)"] = "Earthen",
}

-- Helper: Normalize localized tag to English canonical name
local function NormalizeTagToEnglish(tagName)
    if not tagName then return nil end
    -- Check reverse lookup first
    if TAG_TO_ENGLISH[tagName] then
        return TAG_TO_ENGLISH[tagName]
    end
    -- Already English or unknown - return as-is
    return tagName
end

-- Helper to check if a tag belongs to a category
local function GetTagCategory(tagName)
    -- Normalize to English first
    local englishTag = NormalizeTagToEnglish(tagName)
    for category, tags in pairs(TAG_CATEGORIES) do
        for _, tag in ipairs(tags) do
            if tag == englishTag then
                return category
            end
        end
    end
    return "Other"
end

-- Helper to get first word of a tag name for "Other" category display
-- e.g., "Table Tables" → "Table", "Shelf Shelves" → "Shelf"
local function GetFirstWord(tagName)
    return tagName:match("^(%S+)") or tagName
end

-- Filter buttons use HDG_UIHelpers.CreateFilterButton (default options: height=16, inactive text #B0B0B0)

-- Refresh tag filter buttons (two-row category system)
-- Category buttons (row 1) are created once and cached; tag buttons (row 2) use a pool
local categoryButtonsCreated = false
local tagButtonPool = {} -- reusable pool for row 2 buttons

function HDG_DecorPreview:RefreshTagFilters()
    if not previewPanel or not previewPanel.categoryRow then return end

    -- Get items to extract tags and counts
    local success, items = pcall(GetAllDecorItems)
    if not success then return end

    local tags = GetUniqueTags(items)
    cachedTags = tags

    -- Count active filters
    local activeCount = 0
    for _ in pairs(self.tagFilters or {}) do
        activeCount = activeCount + 1
    end

    local selectedCategory = self.selectedCategory or "Crafted"

    -- Row 1: Category buttons — create once, then just update state
    local categoryDefs = {
        { key = "All", display = L["CAT_ALL"] or "All" },
        { key = "Crafted", display = L["CAT_CRAFTED"] or "Crafted" },
        { key = "Sizes", display = L["CAT_SIZES"] or "Sizes" },
        { key = "Factions", display = L["CAT_FACTIONS"] or "Factions" },
        { key = "Styles", display = L["CAT_STYLES"] or "Styles" },
        { key = "Expansions", display = L["CAT_EXPANSIONS"] or "Expansions" },
        { key = "Other", display = L["CAT_OTHER"] or "Other" },
    }

    if not categoryButtonsCreated then
        categoryButtonsCreated = true
        previewPanel.categoryButtons = {}
        local xOffset = 38

        for _, catDef in ipairs(categoryDefs) do
            local displayText = catDef.display
            if catDef.key == "All" and activeCount > 0 then
                displayText = (L["CAT_ALL"] or "All") .. "*"
            end

            local textWidth = GetTextWidth(displayText)
            local btnWidth = math.max(textWidth + 22, 40)

            local isActive = (selectedCategory == catDef.key)
            local btn = HDG_UIHelpers.CreateFilterButton(
                previewPanel.categoryRow,
                displayText,
                btnWidth,
                isActive,
                function()
                    HDG_DecorPreview.selectedCategory = catDef.key
                    HDG_DecorPreview.tagFilters = {}
                    HDG_DecorPreview.professionFilter = nil
                    HDG_DecorPreview:RefreshList()
                    HDG_DecorPreview:RefreshTagFilters()
                end
            )
            btn:SetPoint("LEFT", xOffset, 0)
            btn.catKey = catDef.key
            btn.baseDisplay = catDef.display
            table.insert(previewPanel.categoryButtons, btn)
            xOffset = xOffset + btnWidth + 2
        end
    end

    -- Update category button states
    for _, btn in ipairs(previewPanel.categoryButtons) do
        local isActive = (selectedCategory == btn.catKey)
        local displayText = btn.baseDisplay
        if btn.catKey == "All" and activeCount > 0 then
            displayText = (L["CAT_ALL"] or "All") .. "*"
        end
        HDG_UIHelpers.UpdateFilterButtonState(btn, isActive, displayText)
    end

    -- Row 2: Tag buttons — pool pattern (hide all, then show/configure needed ones)
    -- Hide all pooled tag buttons first
    for _, btn in ipairs(tagButtonPool) do
        btn:Hide()
    end
    previewPanel.tagButtons = {}

    -- Show/hide "Tags:" label
    if previewPanel.tagsLabel then
        if selectedCategory ~= "All" then
            previewPanel.tagsLabel:Show()
        else
            previewPanel.tagsLabel:Hide()
        end
    end

    if selectedCategory == "All" then return end

    -- Helper: acquire a tag button from pool or create a new one
    local poolIndex = 0
    local function AcquireTagButton(displayName, btnWidth, isActive, onClick)
        poolIndex = poolIndex + 1
        local btn = tagButtonPool[poolIndex]
        if not btn then
            btn = HDG_UIHelpers.CreateFilterButton(previewPanel.tagRow, displayName, btnWidth, isActive, onClick)
            tagButtonPool[poolIndex] = btn
        else
            btn:SetParent(previewPanel.tagRow)
            btn:SetWidth(btnWidth)
            btn:SetScript("OnClick", onClick)
            HDG_UIHelpers.UpdateFilterButtonState(btn, isActive, displayName)
            btn:Show()
        end
        return btn
    end

    if selectedCategory == "Crafted" then
        -- Profession sub-filters
        local professionCounts = {}
        local professionOrder = { "Alchemy", "Blacksmithing", "Enchanting", "Engineering",
                                  "Inscription", "Jewelcrafting", "Leatherworking", "Tailoring", "Cooking" }
        for _, item in ipairs(items) do
            if not item.isCatalogItem and item.profession then
                professionCounts[item.profession] = (professionCounts[item.profession] or 0) + 1
            end
        end

        local xOffset = 38
        local maxWidth = 780

        for _, profName in ipairs(professionOrder) do
            local count = professionCounts[profName]
            if count and count > 0 then
                local displayName = GetLocalizedProfession(profName)
                local textWidth = GetTextWidth(displayName)
                local btnWidth = math.min(textWidth + 15, CONFIG.PREVIEW_TAB.CATEGORY_BUTTON_MAX_WIDTH or 80)

                if xOffset + btnWidth > maxWidth then break end

                local isActive = self.professionFilter == profName
                local btn = AcquireTagButton(displayName, btnWidth, isActive, function()
                    if HDG_DecorPreview.professionFilter == profName then
                        HDG_DecorPreview.professionFilter = nil
                    else
                        HDG_DecorPreview.professionFilter = profName
                    end
                    HDG_DecorPreview:RefreshTagFilters()
                    HDG_DecorPreview:RefreshList()
                end)
                btn:SetPoint("LEFT", xOffset, 0)

                btn:SetScript("OnEnter", function(self)
                    if not self.isActive then
                        local hoverScheme = GetScheme()
                        if hoverScheme then
                            self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, 0.9)
                        else
                            self:SetBackdropColor(0.15, 0.15, 0.18, 0.9)
                        end
                    end
                    GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                    GameTooltip:AddLine(profName, 1, 0.82, 0.3)
                    GameTooltip:AddLine(string.format("%d recipes", count), 0.7, 0.7, 0.7)
                    GameTooltip:Show()
                end)
                btn:SetScript("OnLeave", function(self)
                    if not self.isActive then
                        local leaveScheme = GetScheme()
                        if leaveScheme then
                            self:SetBackdropColor(leaveScheme.button_inactive.r, leaveScheme.button_inactive.g, leaveScheme.button_inactive.b, 0.8)
                        else
                            self:SetBackdropColor(0.1, 0.1, 0.12, 0.8)
                        end
                    end
                    GameTooltip:Hide()
                end)

                table.insert(previewPanel.tagButtons, btn)
                xOffset = xOffset + btnWidth + 2
            end
        end
    else
        -- Standard tag categories
        local xOffset = 38
        local maxWidth = 780

        local categoryTagList = {}
        for _, tagData in ipairs(tags) do
            local cat = GetTagCategory(tagData.name)
            if cat == selectedCategory then
                table.insert(categoryTagList, { name = tagData.name, count = tagData.count })
            end
        end

        if selectedCategory == "Expansions" then
            table.sort(categoryTagList, function(a, b)
                local orderA = HDG_ExpansionData.GetSortOrder(NormalizeTagToEnglish(a.name)) or 99
                local orderB = HDG_ExpansionData.GetSortOrder(NormalizeTagToEnglish(b.name)) or 99
                return orderA < orderB
            end)
        else
            table.sort(categoryTagList, function(a, b) return a.count > b.count end)
        end

        for _, tagData in ipairs(categoryTagList) do
            local tagName = tagData.name
            local count = tagData.count
            if count and count > 0 then
                local englishTag = NormalizeTagToEnglish(tagName)
                local displayName = tagName
                if selectedCategory == "Expansions" then
                    local short = HDG_ExpansionData.GetShortName(englishTag)
                    if short then displayName = short end
                elseif FACTION_TAG_SHORT[englishTag] then
                    displayName = FACTION_TAG_SHORT[englishTag]
                elseif selectedCategory == "Other" then
                    displayName = GetFirstWord(tagName)
                end

                local textWidth = GetTextWidth(displayName)
                local btnWidth = math.min(textWidth + 15, CONFIG.PREVIEW_TAB.TAG_BUTTON_MAX_WIDTH or 70)

                if xOffset + btnWidth > maxWidth then break end

                local isActive = self.tagFilters and self.tagFilters[tagName]
                local btn = AcquireTagButton(displayName, btnWidth, isActive, function()
                    local wasActive = HDG_DecorPreview.tagFilters[tagName]
                    HDG_DecorPreview.tagFilters = {}
                    if not wasActive then
                        HDG_DecorPreview.tagFilters[tagName] = true
                    end
                    HDG_DecorPreview:RefreshTagFilters()
                    HDG_DecorPreview:RefreshList()
                end)
                btn:SetPoint("LEFT", xOffset, 0)
                btn.tagName = tagName

                -- Apply expansion color to button text
                if selectedCategory == "Expansions" then
                    local expColor = HDG_ExpansionData.GetColor(englishTag)
                    if expColor then
                        local colorHex = string.format("%02x%02x%02x", expColor[1] * 255, expColor[2] * 255, expColor[3] * 255)
                        if isActive then
                            btn.text:SetText("|cFF" .. colorHex .. displayName .. "|r")
                        else
                            local dimHex = string.format("%02x%02x%02x", math.min(255, expColor[1] * 220), math.min(255, expColor[2] * 220), math.min(255, expColor[3] * 220))
                            btn.text:SetText("|cFF" .. dimHex .. displayName .. "|r")
                        end
                    end
                end

                -- Tooltip with full name and count
                local tooltipR, tooltipG, tooltipB = 1, 0.82, 0.3
                if selectedCategory == "Expansions" then
                    local expColor = HDG_ExpansionData.GetColor(englishTag)
                    if expColor then tooltipR, tooltipG, tooltipB = expColor[1], expColor[2], expColor[3] end
                end
                btn:SetScript("OnEnter", function(self)
                    if not self.isActive then
                        local hoverScheme = GetScheme()
                        if hoverScheme then
                            self:SetBackdropColor(hoverScheme.button_hover.r, hoverScheme.button_hover.g, hoverScheme.button_hover.b, 0.9)
                        else
                            self:SetBackdropColor(0.15, 0.15, 0.18, 0.9)
                        end
                    end
                    GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                    GameTooltip:AddLine(tagName, tooltipR, tooltipG, tooltipB)
                    GameTooltip:AddLine(string.format("%d items", count), 0.7, 0.7, 0.7)
                    GameTooltip:Show()
                end)
                btn:SetScript("OnLeave", function(self)
                    if not self.isActive then
                        local leaveScheme = GetScheme()
                        if leaveScheme then
                            self:SetBackdropColor(leaveScheme.button_inactive.r, leaveScheme.button_inactive.g, leaveScheme.button_inactive.b, 0.8)
                        else
                            self:SetBackdropColor(0.1, 0.1, 0.12, 0.8)
                        end
                    end
                    GameTooltip:Hide()
                end)

                table.insert(previewPanel.tagButtons, btn)
                xOffset = xOffset + btnWidth + 2
            end
        end
    end
end

-- Refresh just the item list
function HDG_DecorPreview:RefreshList()
    if not previewPanel then
        return
    end
    if not previewPanel.listContent then
        return
    end

    local content = previewPanel.listContent

    -- Release pooled rows for reuse (instead of destroying)
    rowPool:ReleaseAll(function(row)
        if row.nameText then
            row.nameText:SetText("")
        end
    end)

    local success, items = pcall(GetAllDecorItems)
    if not success then
        return
    end

    local yOffset = 0
    local index = 0
    local collectedCount = 0
    local listWidth = CONFIG.PREVIEW_TAB.LIST_CONTENT_WIDTH

    for _, itemData in ipairs(items) do
        -- Use shared filter helper
        if ItemPassesFilters(itemData) then
            index = index + 1
            -- Track collected count for header stats
            if HDG_Data.IsDecorCollected(itemData.decorID) then
                collectedCount = collectedCount + 1
            end
            local ok, newOffset = pcall(CreateItemListEntry, content, listWidth, yOffset, itemData, index)
            if ok then
                yOffset = newOffset
            end
        end
    end

    content:SetHeight(math.abs(yOffset) + 10)

    -- Update title with count and collection progress
    if previewPanel.headerTitle then
        local pct = index > 0 and math.floor((collectedCount / index) * 100) or 0
        local pctColor = pct >= 100 and HDG.Theme.C_Success() or (pct >= 50 and HDG.Theme.C_Accent() or HDG.Theme.C_Text())
        previewPanel.headerTitle:SetText(string.format(
            "%sItems|r %s(%d)|r  %s%d/%d (%d%%)|r",
            HDG.Theme.C_Text(), HDG.Theme.C_Dim(), index, pctColor, collectedCount, index, pct
        ))
    end

    -- Update header icon colors for current theme (use text color on light themes for contrast)
    local iconScheme = GetScheme()
    if iconScheme then
        local iconColor = iconScheme.isLight and iconScheme.text or iconScheme.text_dim
        if previewPanel.headerCraftableIcon then
            previewPanel.headerCraftableIcon:SetVertexColor(iconColor.r, iconColor.g, iconColor.b)
        end
        if previewPanel.headerCollectedIcon then
            previewPanel.headerCollectedIcon:SetVertexColor(iconColor.r, iconColor.g, iconColor.b)
        end
    end
end

-- Update model and info for selected item
function HDG_DecorPreview:UpdateModelAndInfo(itemData)
    if not previewPanel then return end
    UpdateModelDisplay(itemData)
    UpdateInfoPane(previewPanel.infoPanel, itemData)
end

-- Full refresh
function HDG_DecorPreview:Refresh()
    -- Guard: don't refresh if not initialized yet
    if not CONFIG then return end

    if not previewPanel then
        self:CreatePanel()
    end

    self:RefreshTagFilters()
    self:RefreshList()
    
    -- If we have a selected item, update its display
    if selectedDecorID then
        local items = GetAllDecorItems()
        for _, itemData in ipairs(items) do
            if itemData.decorID == selectedDecorID then
                self:UpdateModelAndInfo(itemData)
                break
            end
        end
    end
end

-- Show the panel
function HDG_DecorPreview:Show()
    if not previewPanel then
        self:CreatePanel()
    end
    self:Refresh()
    previewPanel:Show()
end

-- Hide the panel
function HDG_DecorPreview:Hide()
    if previewPanel then
        previewPanel:Hide()
    end
end

-- Check if panel is shown
function HDG_DecorPreview:IsShown()
    return previewPanel and previewPanel:IsShown()
end

-- Preview a specific decor item by name
-- This is the main preview function - switches to Preview tab, filters by name, and refreshes
-- Optional itemID and decorID can be passed for direct 3D preview (bypasses search)
function HDG_DecorPreview:PreviewByName(itemName, itemID, decorID)
    if not itemName or itemName == "" then return false end

    -- Ensure we have a reference to mainFrame
    local frame = mainFrame or _G["HDG_MainFrame"]
    if not frame then
        print(HDG.Theme.C_Error() .. "[Housing Decor Guide]|r Main window not available. Open /hdg first.")
        return false
    end

    -- Ensure CONFIG is available
    if not CONFIG then
        if addon and addon.CONFIG then
            CONFIG = addon.CONFIG
        end
    end
    if not CONFIG then
        print(HDG.Theme.C_Error() .. "[Housing Decor Guide]|r Module not initialized. Try reopening /hdg.")
        return false
    end

    if not mainFrame then
        mainFrame = frame
    end

    -- Switch to Decor Preview tab
    if HDG_Settings then
        HDG_Settings.activeTab = "preview"
    end
    if HDG_UI and HDG_UI.UpdateTabs then
        HDG_UI.UpdateTabs()
    end

    -- Ensure panel is created first
    if not previewPanel then
        self:CreatePanel()
    end

    -- Enable "My Collection" mode to show non-craftable items too
    self.showAllDecor = true

    -- Clear other filters but use search to show only this item
    self.categoryFilter = nil
    self.professionFilter = nil
    self.uncollectedOnly = false
    self.tagFilters = {}

    -- Set search filter to the item name
    self.searchFilter = itemName:lower()

    -- Update the search box UI to show the search term
    if previewPanel and previewPanel.searchBox then
        previewPanel.searchBox:SetText(itemName)
    end

    -- Find and select the matching item for 3D preview BEFORE refresh
    local searchLower = itemName:lower()
    local foundItemData = nil

    -- If decorID or itemID was passed directly, use it (fastest path)
    if decorID or itemID then
        selectedDecorID = decorID
        foundItemData = {
            name = itemName,
            itemID = itemID,
            decorID = decorID,
        }
    end

    -- If no IDs passed, check HDG_DecorDB directly (always available, unlike async API cache)
    if not foundItemData and HDG_DecorDB then
        for spellID, recipe in pairs(HDG_DecorDB) do
            if recipe.name and recipe.name:lower():find(searchLower, 1, true) then
                selectedDecorID = recipe.decorID
                -- Build minimal itemData for UpdateModelAndInfo
                foundItemData = {
                    name = recipe.name,
                    itemID = recipe.itemID,
                    decorID = recipe.decorID,
                    spellID = spellID,
                }
                break
            end
        end
    end

    -- Fallback: check API cache if not found in DecorDB
    if not foundItemData then
        local items = GetAllDecorItems()
        for _, itemData in ipairs(items) do
            local checkName = itemData.name or ""
            if checkName:lower():find(searchLower, 1, true) then
                selectedDecorID = itemData.decorID
                foundItemData = itemData
                break
            end
        end
    end

    -- Refresh will update the list AND call UpdateModelAndInfo for selectedDecorID
    self:Refresh()

    -- If Refresh didn't find it (cache timing issue), force update with our found data
    if foundItemData then
        self:UpdateModelAndInfo(foundItemData)
    end

    return true
end

-- Convenience wrapper: Preview by itemID (looks up name then calls PreviewByName with itemID)
function HDG_DecorPreview:PreviewByItemID(itemID)
    if not itemID then return false end

    -- Get item name from WoW API
    local itemName = C_Item.GetItemNameByID(itemID)
    if itemName then
        return self:PreviewByName(itemName, itemID, nil)
    end

    -- Fallback: search HDG_DecorDB for the name and decorID
    if HDG_DecorDB then
        for _, recipe in pairs(HDG_DecorDB) do
            if recipe.itemID == itemID and recipe.name then
                return self:PreviewByName(recipe.name, itemID, recipe.decorID)
            end
        end
    end

    return false
end
