local addonName, privateDB = ...
local HDG = _G.HousingDecorGuide
local Log = privateDB.Log
local LOG_LEVEL = privateDB.LOG_LEVEL

-- ============================================================================
-- HOUSE EDITOR TAB - Style Browser for Blizzard Housing Panel
-- Injects an "HDG Styles" tab into the Blizzard HouseEditorStorageFrame
-- ============================================================================

local MODULE_NAME = "HouseEditorTab"
local injected = false
local hdgTabID = nil

-- Shared UI state
local hdgContentFrame = nil
local modeFrame = nil
local sidebarChild = nil   -- single sidebar scroll child
local gridScrollChild = nil -- single grid scroll child
local gridButtons = {}
local sidebarButtons = {}
local emptyLabel = nil
local currentMode = "styles"
local currentStyleId = nil
local currentShoppingListId = nil
local placedFromHDGTab = false

-- Hover preview (3D model, lazy-created)
local hoverPreviewFrame = nil

local function GetPreviewFrame()
    if hoverPreviewFrame then return hoverPreviewFrame end
    local UIHelpers = HDG_UIHelpers or (privateDB and privateDB.HDG_UIHelpers)
    if not UIHelpers or not UIHelpers.CreateDecorPreview then return nil end
    hoverPreviewFrame = UIHelpers.CreateDecorPreview(UIParent, {
        squareAspect = true, showOverlays = false, showControls = false, showCorbels = false,
    })
    hoverPreviewFrame:SetSize(150, 150)
    hoverPreviewFrame:SetFrameStrata("TOOLTIP")
    hoverPreviewFrame:Hide()
    if hoverPreviewFrame.placeholder then hoverPreviewFrame.placeholder:Hide() end
    return hoverPreviewFrame
end

-- Layout constants
local SIDEBAR_WIDTH = 140
local SIDEBAR_BTN_HEIGHT = 20
local ICON_SIZE = 64
local ICON_PADDING = 4

-- ============================================================================
-- DYE CACHE (catalog searcher scan for dyed variant detection)
-- ============================================================================
-- GetCatalogEntryInfoByItem/ByRecordID always return empty dye data.
-- Only the catalog searcher returns separate entries per dyed variant.
-- We run one owned-items scan and cache: decorID -> { {qty, numPlaced, dyeNames} }

local dyeCache = {}        -- decorID -> array of dyed variant info
local dyeCacheDirty = true
local lastDisplayItems = nil -- remember for re-expand after async cache

local function ScanDyeCache(onComplete)
    if not C_HousingCatalog or not C_HousingCatalog.CreateCatalogSearcher then
        if onComplete then onComplete() end
        return
    end
    local searcher = C_HousingCatalog.CreateCatalogSearcher()
    searcher:SetOwnedOnly(true)
    searcher:SetResultsUpdatedCallback(function()
        dyeCache = {}
        local results = searcher:GetCatalogSearchResults()
        if results then
            for _, entry in ipairs(results) do
                local decorID = entry.recordID
                if decorID then
                    local subtypeId = entry.subtypeIdentifier or 0
                    local isDyed = subtypeId ~= 0
                    if isDyed then
                        local ok, info = pcall(C_HousingCatalog.GetCatalogEntryInfo, entry)
                        if ok and info then
                            if not dyeCache[decorID] then dyeCache[decorID] = {} end
                            dyeCache[decorID][#dyeCache[decorID] + 1] = {
                                qty = info.quantity or 0,
                                numPlaced = info.numPlaced or 0,
                                dyeNames = info.customizations or {},
                                entryID = info.entryID, -- compound key with subtypeIdentifier for placement
                            }
                        end
                    end
                end
            end
        end
        dyeCacheDirty = false
        if onComplete then onComplete() end
    end)
    searcher:RunSearch()
end

-- Expand displayItems: split items with dyed variants into base + dyed cells
local function ExpandDyeVariants(items)
    if not items or not next(dyeCache) then return items end
    local expanded = {}
    for _, item in ipairs(items) do
        local variants = dyeCache[item.decorID]
        if variants then
            -- Sum dyed variant qty/placed to subtract from base totals
            local dyedQty, dyedPlaced = 0, 0
            for _, v in ipairs(variants) do
                dyedQty = dyedQty + v.qty
                dyedPlaced = dyedPlaced + v.numPlaced
            end
            -- Base entry with adjusted counts
            expanded[#expanded + 1] = {
                itemID = item.itemID, decorID = item.decorID,
                entry = item.entry, tier = item.tier,
                _baseQtyAdjust = dyedQty, _basePlacedAdjust = dyedPlaced,
            }
            -- One cell per dyed variant
            for _, v in ipairs(variants) do
                local dyeLabel = (v.dyeNames and #v.dyeNames > 0) and table.concat(v.dyeNames, ", ") or "Dyed"
                expanded[#expanded + 1] = {
                    itemID = item.itemID, decorID = item.decorID,
                    entry = item.entry, tier = item.tier,
                    _isDyedVariant = true,
                    _dyeQty = v.qty,
                    _dyeNumPlaced = v.numPlaced,
                    _dyeLabel = dyeLabel,
                    _dyeEntryID = v.entryID, -- for StartPlacingNewDecor
                }
            end
        else
            expanded[#expanded + 1] = item
        end
    end
    return expanded
end

-- ============================================================================
-- HORIZONTAL RADIO GROUP
-- ============================================================================

local function CreateHorizontalRadioGroup(parent, options, onChange)
    local frame = CreateFrame("Frame", nil, parent)
    frame._buttons = {}
    frame._selected = nil
    frame._onChange = onChange

    local btnWidth = 82
    local btnHeight = 20
    local spacing = 3

    for i, opt in ipairs(options) do
        local btn = CreateFrame("Button", nil, frame)
        btn:SetSize(btnWidth, btnHeight)
        btn:SetPoint("LEFT", (i - 1) * (btnWidth + spacing), 0)

        local indicator = btn:CreateTexture(nil, "ARTWORK")
        indicator:SetSize(10, 10)
        indicator:SetPoint("LEFT", 2, 0)
        indicator:SetColorTexture(0.18, 0.17, 0.12, 1)
        btn._indicator = indicator

        local dot = btn:CreateTexture(nil, "OVERLAY")
        dot:SetSize(5, 5)
        dot:SetPoint("CENTER", indicator, "CENTER")
        dot:SetColorTexture(0.961, 0.722, 0.0, 1)
        dot:Hide()
        btn._dot = dot

        local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("LEFT", indicator, "RIGHT", 4, 0)
        label:SetText(opt.text or opt)
        label:SetTextColor(0.8, 0.78, 0.7)
        btn._label = label

        btn._value = opt.value or opt
        btn._index = i

        btn:SetScript("OnClick", function()
            frame:SetSelected(i)
            if frame._onChange then
                frame._onChange(btn._value, i)
            end
        end)

        frame._buttons[i] = btn
    end

    local totalWidth = #options * btnWidth + (#options - 1) * spacing
    frame:SetSize(totalWidth, btnHeight)

    function frame:SetSelected(index)
        self._selected = index
        for j, b in ipairs(self._buttons) do
            if j == index then
                b._dot:Show()
                b._indicator:SetColorTexture(0.78, 0.53, 0.04, 1)
                b._label:SetTextColor(1, 0.82, 0)
            else
                b._dot:Hide()
                b._indicator:SetColorTexture(0.18, 0.17, 0.12, 1)
                b._label:SetTextColor(0.8, 0.78, 0.7)
            end
        end
    end

    if #options > 0 then
        frame:SetSelected(1)
    end

    return frame
end

-- ============================================================================
-- ICON BUTTON FACTORY
-- ============================================================================

local function CreateIconButton(parent, index)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(ICON_SIZE, ICON_SIZE)

    local icon = btn:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", 3, -3)
    icon:SetPoint("BOTTOMRIGHT", -3, 3)
    btn._icon = icon

    local borderOverlay = btn:CreateTexture(nil, "BORDER")
    borderOverlay:SetAllPoints()
    borderOverlay:SetColorTexture(0.2, 0.2, 0.18, 1)
    btn._borderOverlay = borderOverlay

    local qty = btn:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
    qty:SetPoint("BOTTOMRIGHT", -4, 4)
    qty:SetTextColor(1, 1, 1)
    qty:Hide()
    btn._qty = qty

    -- Placement restriction border (4 thin edges, 2px)
    local function MakeBorderEdge(anchor1, p1, x1, y1, anchor2, p2, x2, y2, w, h)
        local t = btn:CreateTexture(nil, "OVERLAY", nil, 1)
        t:SetPoint(anchor1, btn, p1, x1, y1)
        t:SetPoint(anchor2, btn, p2, x2, y2)
        if w then t:SetWidth(w) end
        if h then t:SetHeight(h) end
        t:SetColorTexture(1.0, 0.5, 0.2, 0.9)
        t:Hide()
        return t
    end
    btn._placeBorder = {
        MakeBorderEdge("TOPLEFT", "TOPLEFT", 0, 0, "TOPRIGHT", "TOPRIGHT", 0, 0, nil, 2),
        MakeBorderEdge("BOTTOMLEFT", "BOTTOMLEFT", 0, 0, "BOTTOMRIGHT", "BOTTOMRIGHT", 0, 0, nil, 2),
        MakeBorderEdge("TOPLEFT", "TOPLEFT", 0, 0, "BOTTOMLEFT", "BOTTOMLEFT", 0, 0, 2, nil),
        MakeBorderEdge("TOPRIGHT", "TOPRIGHT", 0, 0, "BOTTOMRIGHT", "BOTTOMRIGHT", 0, 0, 2, nil),
    }

    -- Dye indicator (Blizzard atlas, top-left corner -- only shown for dyed items)
    local dyeIcon = btn:CreateTexture(nil, "OVERLAY")
    dyeIcon:SetSize(14, 14)
    dyeIcon:SetPoint("TOPLEFT", 1, -1)
    dyeIcon:Hide()
    btn._dyeIcon = dyeIcon

    local hl = btn:CreateTexture(nil, "HIGHLIGHT")
    hl:SetAllPoints()
    hl:SetColorTexture(1, 1, 1, 0.15)

    btn:SetScript("OnEnter", function(self)
        if self._itemName then
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(self._itemName, 1, 1, 1)
            local owned = self._ownedQty or 0
            local placed = self._numPlaced or 0
            local total = owned + placed
            GameTooltip:AddLine("Owned: " .. total .. " (Placed: " .. placed .. ", Storage: " .. owned .. ")", 0, 1, 0)
            if self._placementCost and self._placementCost > 0 then
                GameTooltip:AddLine("Placement cost: " .. self._placementCost, 0.6, 0.6, 0.5)
            end
            if self._dyeLabel then
                GameTooltip:AddLine("Dyes: " .. self._dyeLabel, 0.4, 0.8, 1.0)
            end
            if self._placeLabel then
                GameTooltip:AddLine(self._placeLabel, 1.0, 0.5, 0.2)
            end
            GameTooltip:Show()
        end
        -- 3D preview next to hovered icon (must parent to HouseEditorFrame for visibility)
        if self._itemID then
            local preview = GetPreviewFrame()
            if preview then
                if HouseEditorFrame then
                    preview:SetParent(HouseEditorFrame)
                    preview:SetFrameStrata("DIALOG")
                end
                preview:ClearAllPoints()
                local storagePanel = HouseEditorFrame and HouseEditorFrame.StoragePanel
                preview:SetPoint("BOTTOMLEFT", storagePanel or hdgContentFrame, "BOTTOMRIGHT", 4, 0)
                preview:LoadItem(self._itemID)
                preview:Show()
            end
        end
    end)
    btn:SetScript("OnLeave", function()
        GameTooltip:Hide()
        if hoverPreviewFrame then hoverPreviewFrame:Hide() end
    end)

    btn._index = index
    btn:Hide()
    return btn
end

-- ============================================================================
-- DATA COLLECTORS
-- ============================================================================

local function EnsureStyleCache()
    local StyleEngine = privateDB.HDG_StyleEngine
    if not StyleEngine then return nil end
    if not StyleEngine:IsCacheReady() then
        StyleEngine:BuildCache()
    end
    return StyleEngine
end

-- Check ownership via catalog API (quantity > 0 means in storage or placed)
local function IsItemOwned(itemID, decorID)
    if decorID and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
        local info = C_HousingCatalog.GetCatalogEntryInfoByRecordID(1, decorID, true)
        if info and (info.quantity or 0) + (info.numPlaced or 0) > 0 then return true end
    end
    if itemID and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
        local info = C_HousingCatalog.GetCatalogEntryInfoByItem(itemID, true)
        if info and (info.quantity or 0) + (info.numPlaced or 0) > 0 then return true end
    end
    return false
end

-- Collect items from StyleEngine buckets
-- signatureOnly: only "first" tier items; ownedOnly: filter to collected items
local function CollectStyleItems(styleId, ownedOnly, signatureOnly)
    local StyleEngine = EnsureStyleCache()
    if not StyleEngine then return {} end

    local items = StyleEngine:GetStyleItems(styleId)
    if not items then return {} end

    local displayItems = {}
    for bucketKey, bucket in pairs(items) do
        if bucketKey ~= "_byItemID" and type(bucket) == "table" and #bucket > 0 then
            if not signatureOnly or bucketKey == "first" or bucketKey == "curated" then
                for _, item in ipairs(bucket) do
                    if not ownedOnly or IsItemOwned(item.itemID, item.decorID) then
                        displayItems[#displayItems + 1] = { itemID = item.itemID, decorID = item.decorID, entry = item.entry, tier = bucketKey }
                    end
                end
            end
        end
    end
    return displayItems
end

-- Collect items from a shopping list (owned-only)
local function CollectShoppingItems(listId)
    if not HDG.Store then return {} end
    local list = HDG.Store.state.vendorShoppingLists[listId]
    if not list or not list.items then return {} end

    local displayItems = {}
    for _, entry in ipairs(list.items) do
        if entry.itemID then
            local decorID = nil
            if HDG_AllDecorDB and HDG_AllDecorDB[entry.itemID] then
                decorID = HDG_AllDecorDB[entry.itemID][1]
            end
            if IsItemOwned(entry.itemID, decorID) then
                displayItems[#displayItems + 1] = {
                    itemID = entry.itemID, decorID = decorID,
                    entry = HDG_AllDecorDB and HDG_AllDecorDB[entry.itemID],
                    tier = "neutral",
                }
            end
        end
    end
    return displayItems
end

-- ============================================================================
-- SIDEBAR DATA PROVIDERS (one per mode)
-- ============================================================================

-- Your Styles: player-created custom styles (non-snapshot)
local function GetCustomStyleSidebarItems()
    if not HDG.Store then return {} end
    local customs, order = HDG.Store:GetCustomStyles()
    if not customs then return {} end

    local items = {}
    local used = {}
    if order then
        for _, key in ipairs(order) do
            local def = customs[key]
            if def and def.tier ~= "snapshot" then
                items[#items + 1] = { value = key, text = def.displayName or key }
                used[key] = true
            end
        end
    end
    for key, def in pairs(customs) do
        if not used[key] and def.tier ~= "snapshot" then
            items[#items + 1] = { value = key, text = def.displayName or key }
        end
    end
    table.sort(items, function(a, b) return a.text:lower() < b.text:lower() end)
    return items
end

-- Shopping: all player shopping lists
local function GetShoppingListSidebarItems()
    if not HDG.Store then return {} end
    local lists = HDG.Store:GetShoppingLists()
    local items = {}
    for _, list in ipairs(lists) do
        local label = (list.name or "List") .. "  (" .. list.count .. ")"
        items[#items + 1] = { value = list.id, text = label }
    end
    table.sort(items, function(a, b) return a.text:lower() < b.text:lower() end)
    return items
end

-- Snapshots: custom styles with tier == "snapshot"
local function GetSnapshotSidebarItems()
    if not HDG.Store then return {} end
    local customs, order = HDG.Store:GetCustomStyles()
    if not customs then return {} end

    local items = {}
    local used = {}
    if order then
        for _, key in ipairs(order) do
            local def = customs[key]
            if def and def.tier == "snapshot" then
                items[#items + 1] = { value = key, text = def.displayName or key }
                used[key] = true
            end
        end
    end
    for key, def in pairs(customs) do
        if not used[key] and def.tier == "snapshot" then
            items[#items + 1] = { value = key, text = def.displayName or key }
        end
    end
    table.sort(items, function(a, b) return a.text:lower() < b.text:lower() end)
    return items
end

-- Themes: built-in styles grouped by category
local function GetThemesSidebarItems()
    if not HDG_StyleDefinitions then return {} end
    local StyleEngine = EnsureStyleCache()

    local groups = { thematic = {}, themed = {}, faction = {} }
    local groupOrder = { "thematic", "themed", "faction" }
    local groupLabels = { thematic = "Room Concepts", themed = "Themed", faction = "Faction" }

    for id, def in pairs(HDG_StyleDefinitions) do
        local t = def.tier or "thematic"
        if groups[t] then
            local count = 0
            if StyleEngine then
                local stats = StyleEngine:GetStyleStats(id)
                if stats then count = (stats.firstCount or 0) + (stats.secondCount or 0) end
            end
            groups[t][#groups[t] + 1] = { value = id, text = def.displayName or id, count = count }
        end
    end

    for _, g in pairs(groups) do
        table.sort(g, function(a, b) return a.text < b.text end)
    end

    local items = {}
    for _, gKey in ipairs(groupOrder) do
        local g = groups[gKey]
        if #g > 0 then
            items[#items + 1] = { value = nil, text = groupLabels[gKey], isHeader = true }
            for _, entry in ipairs(g) do
                items[#items + 1] = entry
            end
        end
    end
    return items
end

-- Collections: auto-resolved item groups from AllDecorDB name patterns
local function GetCollectionsSidebarItems()
    if not HDG_CollectionDefinitions or not HDG_CollectionOrder then return {} end
    local StyleEngine = EnsureStyleCache()
    local items = {}
    for _, collectionId in ipairs(HDG_CollectionOrder) do
        local def = HDG_CollectionDefinitions[collectionId]
        if def then
            local count = 0
            if StyleEngine then
                local stats = StyleEngine:GetStyleStats(collectionId)
                if stats then count = stats.curatedCount or 0 end
            end
            items[#items + 1] = {
                value = collectionId,
                text = def.displayName or collectionId,
                count = count,
            }
        end
    end
    return items
end

-- ============================================================================
-- POPULATE SIDEBAR (shared, single instance)
-- ============================================================================

local function PopulateSidebar(items, selectedValue, onSelect)
    for _, btn in ipairs(sidebarButtons) do btn:Hide() end
    if not sidebarChild then return end
    if not items or #items == 0 then
        sidebarChild:SetHeight(100)
        return
    end

    sidebarChild:SetHeight(math.max(#items * SIDEBAR_BTN_HEIGHT, 100))

    for i, item in ipairs(items) do
        local btn = sidebarButtons[i]
        if not btn then
            btn = CreateFrame("Button", nil, sidebarChild)
            btn:SetSize(SIDEBAR_WIDTH - 14, SIDEBAR_BTN_HEIGHT)

            local label = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            label:SetPoint("LEFT", 4, 0)
            label:SetPoint("RIGHT", -2, 0)
            label:SetJustifyH("LEFT")
            label:SetWordWrap(false)
            btn._label = label

            local hl = btn:CreateTexture(nil, "HIGHLIGHT")
            hl:SetAllPoints()
            hl:SetColorTexture(0.961, 0.722, 0.0, 0.12)

            local sel = btn:CreateTexture(nil, "ARTWORK")
            sel:SetSize(2, SIDEBAR_BTN_HEIGHT - 4)
            sel:SetPoint("LEFT", 1, 0)
            sel:SetColorTexture(0.961, 0.722, 0.0, 1)
            sel:Hide()
            btn._selBar = sel

            sidebarButtons[i] = btn
        end

        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", 2, -(i - 1) * SIDEBAR_BTN_HEIGHT)
        btn._value = item.value

        if item.isHeader then
            btn._label:SetText(item.text)
            btn._label:SetTextColor(1, 0.82, 0)
            btn._selBar:Hide()
            btn:SetScript("OnClick", nil)
            btn:EnableMouse(false)
        else
            btn._label:SetText(item.text)
            btn._label:SetTextColor(0.8, 0.78, 0.7)
            btn:EnableMouse(true)

            btn:SetScript("OnClick", function(self)
                for _, b in ipairs(sidebarButtons) do
                    if b:IsShown() and not b._isHeader then
                        b._selBar:Hide()
                        b._label:SetTextColor(0.8, 0.78, 0.7)
                    end
                end
                self._selBar:Show()
                self._label:SetTextColor(1, 0.82, 0)
                if onSelect then onSelect(self._value) end
            end)

            if item.value == selectedValue then
                btn._selBar:Show()
                btn._label:SetTextColor(1, 0.82, 0)
            end
        end
        btn._isHeader = item.isHeader
        btn:Show()
    end
end

-- ============================================================================
-- POPULATE ICON GRID (shared, single instance)
-- ============================================================================

local function PopulateGrid(displayItems)
    for _, btn in ipairs(gridButtons) do btn:Hide() end
    if not displayItems or #displayItems == 0 then return end

    -- Store original items for re-expand after async dye cache
    lastDisplayItems = displayItems

    -- Kick off async dye cache scan, then re-render with expanded variants
    if dyeCacheDirty then
        ScanDyeCache(function()
            if lastDisplayItems then PopulateGrid(lastDisplayItems) end
        end)
    end

    -- Expand dyed variants into separate cells (no-op if cache empty)
    displayItems = ExpandDyeVariants(displayItems)

    Log(LOG_LEVEL.INFO, MODULE_NAME, "Populating grid: " .. #displayItems .. " items")

    local gridWidth = gridScrollChild and gridScrollChild:GetWidth() or 340
    local cols = math.max(1, math.floor(gridWidth / (ICON_SIZE + ICON_PADDING)))
    local rows = math.ceil(#displayItems / cols)

    if gridScrollChild then
        gridScrollChild:SetHeight(math.max(rows * (ICON_SIZE + ICON_PADDING), 100))
    end

    for i, item in ipairs(displayItems) do
        local btn = gridButtons[i]
        if not btn then
            btn = CreateIconButton(gridScrollChild, i)
            gridButtons[i] = btn
        end

        local row = math.floor((i - 1) / cols)
        local col = (i - 1) % cols
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", col * (ICON_SIZE + ICON_PADDING), -(row * (ICON_SIZE + ICON_PADDING)))

        local iconTexture, itemName, ownedQty, numPlaced, placementCost = nil, nil, 0, 0, 0
        local allowIndoors, allowOutdoors = true, true
        local canCustomize = false

        if C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
            local info = C_HousingCatalog.GetCatalogEntryInfoByItem(item.itemID, true)
            if info then
                iconTexture = info.iconTexture
                itemName = info.name
                ownedQty = info.quantity or 0
                numPlaced = info.numPlaced or 0
                placementCost = info.placementCost or 0
                canCustomize = info.canCustomize or false
                if info.isAllowedIndoors ~= nil then allowIndoors = info.isAllowedIndoors end
                if info.isAllowedOutdoors ~= nil then allowOutdoors = info.isAllowedOutdoors end
            end
        end

        -- Fallback chain: C_Item icon > C_HousingDecor icon > entry table > question mark
        if not iconTexture or iconTexture == 0 then
            if C_Item and C_Item.GetItemIconByID then
                iconTexture = C_Item.GetItemIconByID(item.itemID)
            end
        end
        if not iconTexture or iconTexture == 0 then
            if item.decorID and C_HousingDecor and C_HousingDecor.GetDecorIcon then
                iconTexture = C_HousingDecor.GetDecorIcon(item.decorID)
            end
        end
        if not itemName and item.entry then
            itemName = item.entry.name or item.entry[3] or item.entry[2]
        end

        -- Dyed variant: override qty/name from expansion data
        if item._isDyedVariant then
            ownedQty = item._dyeQty or 0
            numPlaced = item._dyeNumPlaced or 0
            itemName = (itemName or "") .. " (" .. (item._dyeLabel or "Dyed") .. ")"
        elseif item._baseQtyAdjust then
            -- Base entry with dyed siblings: subtract dyed qty from aggregate total
            ownedQty = math.max(0, ownedQty - (item._baseQtyAdjust or 0))
            numPlaced = math.max(0, numPlaced - (item._basePlacedAdjust or 0))
        end

        btn._icon:SetTexture((iconTexture and iconTexture ~= 0) and iconTexture or "Interface\\Icons\\INV_Misc_QuestionMark")

        btn._borderOverlay:SetColorTexture(0, 0, 0, 0.9)

        if ownedQty > 0 then
            btn._qty:SetText(ownedQty)
            btn._qty:Show()
        else
            btn._qty:Hide()
        end

        btn._itemName = itemName or ("Item " .. tostring(item.itemID))
        btn._ownedQty = ownedQty
        btn._numPlaced = numPlaced
        btn._placementCost = placementCost
        btn._canCustomize = canCustomize

        -- Dye indicator: show filled drop on dyed variant cells
        if item._isDyedVariant then
            btn._dyeIcon:SetAtlas("dye-drop_32")
            btn._dyeIcon:Show()
        else
            btn._dyeIcon:Hide()
        end
        btn._allowIndoors = allowIndoors
        btn._allowOutdoors = allowOutdoors
        if not allowIndoors and not allowOutdoors then
            btn._placeLabel = nil
        elseif allowIndoors and not allowOutdoors then
            btn._placeLabel = "Indoor only"
        elseif allowOutdoors and not allowIndoors then
            btn._placeLabel = "Outdoor only"
        else
            btn._placeLabel = nil -- both allowed
        end
        btn._itemID = item.itemID
        btn._decorID = item.decorID
        btn._dyeEntryID = item._dyeEntryID -- nil for base, entryID for dyed variant
        btn._dyeLabel = item._dyeLabel -- nil for base, "Horde Red" / "Red, Blue" for dyed

        -- Placement restriction border: highlight mismatch with current location
        local showBorder = false
        local playerIndoors = IsIndoors()
        if playerIndoors and not allowOutdoors and not allowIndoors then
            showBorder = false -- neither allowed = no border
        elseif playerIndoors and allowOutdoors and not allowIndoors then
            showBorder = true -- player indoors, item outdoor-only
        elseif not playerIndoors and allowIndoors and not allowOutdoors then
            showBorder = true -- player outdoors, item indoor-only
        end
        for _, edge in ipairs(btn._placeBorder) do edge:SetShown(showBorder) end

        -- Click handler - place item via C_HousingBasicMode.StartPlacingNewDecor
        btn:SetScript("OnClick", function(self)
            -- Dyed variant: use cached entryID directly (has correct subtypeIdentifier)
            if self._dyeEntryID then
                local dQty = self._ownedQty or 0
                if dQty < 1 then
                    print("|cFFFF8800[HDG]|r All copies of \"" .. (self._itemName or "?") .. "\" are already placed.")
                    return
                end
                placedFromHDGTab = true
                C_Timer.After(0.1, function()
                    if C_HousingBasicMode and C_HousingBasicMode.StartPlacingNewDecor then
                        C_HousingBasicMode.StartPlacingNewDecor(self._dyeEntryID)
                    end
                end)
                return
            end

            -- Base item: standard lookup
            local info
            if self._decorID and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByRecordID then
                info = C_HousingCatalog.GetCatalogEntryInfoByRecordID(1, self._decorID, true)
            end
            if not info and self._itemID and C_HousingCatalog and C_HousingCatalog.GetCatalogEntryInfoByItem then
                info = C_HousingCatalog.GetCatalogEntryInfoByItem(self._itemID, true)
            end
            if not info or not info.entryID then
                print("|cFFFF8800[HDG]|r Item not found in catalog.")
                return
            end
            if (info.quantity or 0) < 1 then
                if (info.numPlaced or 0) > 0 then
                    print("|cFFFF8800[HDG]|r All copies of \"" .. (self._itemName or "?") .. "\" are already placed.")
                else
                    print("|cFFFF8800[HDG]|r You don't own \"" .. (self._itemName or "?") .. "\" yet.")
                end
                return
            end
            -- Indoor/outdoor placement check
            if C_Housing and C_Housing.IsInsideHouse then
                local isInside = C_Housing.IsInsideHouse()
                if isInside and not self._allowIndoors then
                    print("|cFFFF8800[HDG]|r \"" .. (self._itemName or "?") .. "\" is outdoor only.")
                    return
                elseif not isInside and not self._allowOutdoors then
                    print("|cFFFF8800[HDG]|r \"" .. (self._itemName or "?") .. "\" is indoor only.")
                    return
                end
            end
            local entryID = info.entryID
            placedFromHDGTab = true
            C_Timer.After(0.1, function()
                if C_HousingBasicMode and C_HousingBasicMode.StartPlacingNewDecor then
                    C_HousingBasicMode.StartPlacingNewDecor(entryID)
                end
            end)
        end)

        btn:Show()
    end
end

-- ============================================================================
-- MODE SWITCHING (repopulates single sidebar + grid)
-- ============================================================================

local function RefreshCurrentMode()
    if not sidebarChild then return end

    local sidebarItems, selectedValue, onSelect, emptyText

    if currentMode == "styles" then
        sidebarItems = GetCustomStyleSidebarItems()
        selectedValue = currentStyleId
        emptyText = "No custom styles yet.\nCreate styles in the HDG Styles tab."
        onSelect = function(value)
            currentStyleId = value
            PopulateGrid(CollectStyleItems(value, true, true)) -- owned + signature only
        end

    elseif currentMode == "shopping" then
        sidebarItems = GetShoppingListSidebarItems()
        selectedValue = currentShoppingListId
        emptyText = "No shopping lists.\nAdd items via the Acquisition tab."
        onSelect = function(value)
            currentShoppingListId = value
            PopulateGrid(CollectShoppingItems(value))
        end

    elseif currentMode == "snapshots" then
        sidebarItems = GetSnapshotSidebarItems()
        selectedValue = currentStyleId
        emptyText = "No house snapshots yet.\nSave one from the HDG Styles tab."
        onSelect = function(value)
            currentStyleId = value
            PopulateGrid(CollectStyleItems(value, true)) -- owned only
        end

    elseif currentMode == "themes" then
        sidebarItems = GetThemesSidebarItems()
        selectedValue = currentStyleId
        emptyText = nil
        onSelect = function(value)
            currentStyleId = value
            PopulateGrid(CollectStyleItems(value, true))
        end

    elseif currentMode == "collections" then
        sidebarItems = GetCollectionsSidebarItems()
        selectedValue = currentStyleId
        emptyText = nil
        onSelect = function(value)
            currentStyleId = value
            PopulateGrid(CollectStyleItems(value, true)) -- owned only
        end
    end

    -- Empty state
    if emptyLabel then
        if not sidebarItems or #sidebarItems == 0 then
            emptyLabel:SetText(emptyText or "Nothing to show.")
            emptyLabel:Show()
            PopulateGrid({})
        else
            emptyLabel:Hide()
        end
    end

    PopulateSidebar(sidebarItems or {}, selectedValue, onSelect)
end

local function SwitchMode(mode)
    currentMode = mode
    currentStyleId = nil
    currentShoppingListId = nil
    PopulateGrid({}) -- clear grid when switching
    RefreshCurrentMode()
end

-- ============================================================================
-- SHOW/HIDE HDG CONTENT
-- ============================================================================

local function ShowHDGContent(storagePanel)
    if storagePanel.OptionsContainer then storagePanel.OptionsContainer:Hide() end
    if storagePanel.Categories then storagePanel.Categories:Hide() end
    if storagePanel.SearchBox then storagePanel.SearchBox:Hide() end
    if storagePanel.Filters then storagePanel.Filters:Hide() end
    if hdgContentFrame then hdgContentFrame:Show() end
end

local function HideHDGContent(storagePanel)
    if hdgContentFrame then hdgContentFrame:Hide() end
    if storagePanel.OptionsContainer then storagePanel.OptionsContainer:Show() end
    if storagePanel.Categories then storagePanel.Categories:Show() end
    if storagePanel.SearchBox then storagePanel.SearchBox:Show() end
    if storagePanel.Filters then storagePanel.Filters:Show() end
end

-- ============================================================================
-- INJECT TAB
-- ============================================================================

local function InjectTab()
    if injected then return end
    if InCombatLockdown() then return end

    local storagePanel = HouseEditorFrame and HouseEditorFrame.StoragePanel
    if not storagePanel then
        Log(LOG_LEVEL.WARN, MODULE_NAME, "HouseEditorFrame.StoragePanel not found")
        return
    end

    if not storagePanel.AddNamedTab then
        Log(LOG_LEVEL.WARN, MODULE_NAME, "StoragePanel has no AddNamedTab")
        return
    end

    Log(LOG_LEVEL.INFO, MODULE_NAME, "Injecting HDG tab into HouseEditorStorageFrame...")

    -- Main content container
    hdgContentFrame = CreateFrame("Frame", "HDG_HouseEditorContent", storagePanel)
    hdgContentFrame:SetPoint("TOPLEFT", storagePanel, "TOPLEFT", 8, -40)
    hdgContentFrame:SetPoint("BOTTOMRIGHT", storagePanel, "BOTTOMRIGHT", -8, 30)
    hdgContentFrame:Hide()

    -- Header: icon with gold border + title (parented to storagePanel so they sit in the header area)
    local headerIconBorder = storagePanel:CreateTexture(nil, "ARTWORK")
    headerIconBorder:SetSize(24, 24)
    headerIconBorder:SetPoint("TOPLEFT", hdgContentFrame, "TOPLEFT", 2, 23)
    headerIconBorder:SetColorTexture(0.761, 0.561, 0.0, 1)
    headerIconBorder:Hide()

    local headerIcon = storagePanel:CreateTexture(nil, "OVERLAY")
    headerIcon:SetSize(22, 22)
    headerIcon:SetPoint("CENTER", headerIconBorder, "CENTER")
    headerIcon:SetTexture("Interface\\AddOns\\HousingDecorGuide\\textures\\HousingDecorIcon")
    headerIcon:Hide()

    local headerText = storagePanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerText:SetPoint("LEFT", headerIcon, "RIGHT", 4, 0)
    headerText:SetText("Housing Decor Guide")
    headerText:SetTextColor(0.8, 0.78, 0.7)
    headerText:Hide()

    -- Show/hide header with content frame
    hdgContentFrame:HookScript("OnShow", function() headerIconBorder:Show(); headerIcon:Show(); headerText:Show() end)
    hdgContentFrame:HookScript("OnHide", function() headerIconBorder:Hide(); headerIcon:Hide(); headerText:Hide() end)

    -- Horizontal RadioGroup below header
    modeFrame = CreateHorizontalRadioGroup(hdgContentFrame, {
        { text = "Your Styles",  value = "styles" },
        { text = "Shopping",     value = "shopping" },
        { text = "Snapshots",    value = "snapshots" },
        { text = "Themes",       value = "themes" },
        { text = "Collections",  value = "collections" },
    }, function(value)
        SwitchMode(value)
    end)
    modeFrame:SetPoint("TOPLEFT", 4, -6)

    -- Single sidebar + grid content area (below RadioGroup)
    local contentArea = CreateFrame("Frame", nil, hdgContentFrame)
    contentArea:SetPoint("TOPLEFT", 0, -46)
    contentArea:SetPoint("BOTTOMRIGHT", 0, 0)

    -- Left sidebar
    local sidebarBg = CreateFrame("Frame", nil, contentArea)
    sidebarBg:SetPoint("TOPLEFT", 0, 0)
    sidebarBg:SetPoint("BOTTOMLEFT", 0, 0)
    sidebarBg:SetWidth(SIDEBAR_WIDTH)

    local bgTex = sidebarBg:CreateTexture(nil, "BACKGROUND")
    bgTex:SetAllPoints()
    bgTex:SetAtlas("house-chest-nav-bg_primary")

    local sidebarScroll = CreateFrame("ScrollFrame", "HDG_HE_SidebarScroll", sidebarBg, "UIPanelScrollFrameTemplate")
    sidebarScroll:SetPoint("TOPLEFT", 0, -2)
    sidebarScroll:SetPoint("BOTTOMRIGHT", -14, 2)

    sidebarChild = CreateFrame("Frame", nil, sidebarScroll)
    sidebarChild:SetWidth(SIDEBAR_WIDTH - 14)
    sidebarChild:SetHeight(400)
    sidebarScroll:SetScrollChild(sidebarChild)

    -- Empty state label
    emptyLabel = contentArea:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    emptyLabel:SetPoint("CENTER", 0, 0)
    emptyLabel:SetTextColor(0.5, 0.5, 0.45)
    emptyLabel:Hide()

    -- Right side: scrollable icon grid
    local gridScroll = CreateFrame("ScrollFrame", "HDG_HE_GridScroll", contentArea, "UIPanelScrollFrameTemplate")
    gridScroll:SetPoint("TOPLEFT", SIDEBAR_WIDTH + 4, 0)
    gridScroll:SetPoint("BOTTOMRIGHT", -22, 4)

    gridScrollChild = CreateFrame("Frame", nil, gridScroll)
    local initW = gridScroll:GetWidth()
    gridScrollChild:SetWidth(initW > 0 and initW or 400)
    gridScrollChild:SetHeight(400)
    gridScroll:SetScrollChild(gridScrollChild)

    gridScroll:SetScript("OnSizeChanged", function(self, width)
        gridScrollChild:SetWidth(width)
    end)

    -- Default to Your Styles mode (radio button index 1)
    modeFrame:SetSelected(1)
    SwitchMode("styles")

    -- Add our tab
    hdgTabID = storagePanel:AddNamedTab("HDG Styles")
    Log(LOG_LEVEL.INFO, MODULE_NAME, "AddNamedTab returned tabID=" .. tostring(hdgTabID))

    -- Tab callbacks
    if storagePanel.SetTabCallback then
        storagePanel:SetTabCallback(hdgTabID, function()
            ShowHDGContent(storagePanel)
        end)
    end

    if storagePanel.SetTabDeselectCallback then
        storagePanel:SetTabDeselectCallback(hdgTabID, function()
            HideHDGContent(storagePanel)
        end)
    end

    -- Hook SetTab for our custom tab
    local originalSetTab = storagePanel.SetTab
    storagePanel.SetTab = function(self, tabID, isUserAction)
        if tabID == hdgTabID then
            HideHDGContent(self)
            ShowHDGContent(self)
            return
        else
            HideHDGContent(self)
            if originalSetTab then
                return originalSetTab(self, tabID, isUserAction)
            end
        end
    end

    -- Hook TabSystem callback to route our tab
    if storagePanel.TabSystem then
        local origCallback = storagePanel.TabSystem.tabSelectedCallback
        storagePanel.TabSystem:SetTabSelectedCallback(function(tabID, isUserAction)
            if tabID == hdgTabID then
                storagePanel:SetTab(tabID, isUserAction)
                return
            end
            if origCallback then
                return origCallback(tabID, isUserAction)
            end
        end)
    end

    -- Hook StoragePanel OnShow: returns to HDG tab after place/cancel
    storagePanel:HookScript("OnShow", function()
        if placedFromHDGTab and hdgTabID then
            placedFromHDGTab = false
            C_Timer.After(0, function()
                if storagePanel.TabSystem then
                    storagePanel.TabSystem:SetTab(hdgTabID)
                end
            end)
        end
    end)

    -- Hook OptionsContainer OnShow: Blizzard re-shows it on storage events
    if storagePanel.OptionsContainer then
        storagePanel.OptionsContainer:HookScript("OnShow", function(self)
            if hdgContentFrame and hdgContentFrame:IsShown() then
                self:Hide()
            end
        end)
    end

    injected = true
    Log(LOG_LEVEL.INFO, MODULE_NAME, "HDG tab injected! tabID=" .. tostring(hdgTabID))
    Log(LOG_LEVEL.INFO, MODULE_NAME, "House Editor tab injected")
end

-- ============================================================================
-- EVENT HOOK: Wait for Blizzard_HouseEditor to load
-- ============================================================================

local function ReturnToHDGTab()
    if not placedFromHDGTab or not hdgTabID then return end
    placedFromHDGTab = false
    local storagePanel = HouseEditorFrame and HouseEditorFrame.StoragePanel
    if storagePanel and storagePanel.TabSystem then
        C_Timer.After(0.15, function()
            if storagePanel.TabSystem then
                storagePanel.TabSystem:SetTab(hdgTabID)
            end
        end)
    end
end

local hookFrame = CreateFrame("Frame")
hookFrame:RegisterEvent("ADDON_LOADED")
hookFrame:RegisterEvent("HOUSE_EDITOR_MODE_CHANGED")
hookFrame:RegisterEvent("HOUSING_DECOR_PLACE_SUCCESS")
hookFrame:RegisterEvent("HOUSING_DECOR_PLACE_FAILURE")
hookFrame:RegisterEvent("HOUSING_STORAGE_UPDATED")
hookFrame:SetScript("OnEvent", function(self, event, arg1)
    if event == "ADDON_LOADED" and arg1 == "Blizzard_HouseEditor" then
        C_Timer.After(0, InjectTab)
    elseif event == "HOUSE_EDITOR_MODE_CHANGED" then
        if not injected and HouseEditorFrame then
            C_Timer.After(0, InjectTab)
        end
        ReturnToHDGTab()
    elseif event == "HOUSING_DECOR_PLACE_SUCCESS" or event == "HOUSING_DECOR_PLACE_FAILURE" then
        dyeCacheDirty = true
        ReturnToHDGTab()
    elseif event == "HOUSING_STORAGE_UPDATED" then
        -- Refresh grid to update owned counts after placing/picking up decor
        dyeCacheDirty = true
        if injected and hdgContentFrame and hdgContentFrame:IsShown() then
            PopulateGrid()
        end
    end
end)

if C_AddOns.IsAddOnLoaded("Blizzard_HouseEditor") then
    C_Timer.After(0, InjectTab)
end
