-- HDG_StylesTab.lua
-- Style tab UI: landing page (style card grid) + detail view (item list by tier).

local addonName, privateDB = ...

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

-- Module namespace
local StylesTab = {}
privateDB.HDG_StylesTab = StylesTab

StylesTab.initialized = false

-- Local references (set during Init)
local CONFIG
local mainFrame
local StyleEngine

-- Deep copy utility (must be defined early, used by card buttons + editor)
local function DeepCopyTable(orig)
    if type(orig) ~= "table" then return orig end
    local copy = {}
    for k, v in pairs(orig) do
        copy[k] = DeepCopyTable(v)
    end
    return copy
end
StylesTab.DeepCopyTable = DeepCopyTable

-- UI frames
local panel           -- main tab panel
local landingFrame    -- card grid view
local detailFrame     -- single style detail view

-- Serializer reference (set during Init)
local StyleSerializer

-- State
local selectedStyle = nil    -- current style ID in detail view
---@type string|nil
local selectedTier = "first" -- "first", "second", "neutral", "clash"; nil for snapshot/collection
local selectedItemID = nil   -- currently selected item in list
local searchText = ""
local searchDebounceTimer = nil
local uncollectedOnly = false
local collectedOnly = false
local craftableOnly = false
local hideUnreleased = true
local function IsItemHidden(itemID)
    if not hideUnreleased then return false end
    local ic = StyleEngine and StyleEngine:GetItemInfo(itemID)
    return not ic or ic.isUnreleased
end
local selectedRoom = nil -- nil = "All", or room key like "kitchen"
local selectedSource = nil -- nil = "All", or sourceType int like 5 (Vendor)
local selectedLandingCategory = nil -- nil = All, or tier key like "custom", "snapshot", etc.
local selectedShoppingListId = nil -- nil = not viewing a shopping list; set = viewing SL detail
local selectedSessionData    = nil -- nil = not viewing a session; set = { houseGUID, sessionID, displayName }

-- Collapsible tier state
local collapsedTiers = {}

-- Module-level stats cache (dirty-flagged; only rebuilt on collection changes)
local statsCache = {}
local statsCacheDirty = true

-- Detail view mode: "list" or "gallery"
local viewMode = "gallery"
local listFrame

local ROW_HEIGHT = 22
-- Landing-page grid uses taller rows than the item list; tier headers are
-- intermediate. Declared at module scope so the ExtentCalculator callback
-- below (line ~368) resolves the name from its closure, not _G.
local ROW_HEIGHT_TIER_HEADER = 22
local ROW_HEIGHT_LANDING = 43

-- Populate a ScrollBox-allocated Button frame with item-list widgets.
-- Called once per frame (guarded by _hdgConstructed). Mutates `row` in place.
local function ConstructItemListRow(row)
    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(18, 18)
    row.icon:SetPoint("LEFT", 4, 0)

    row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.nameText:SetPoint("LEFT", row.icon, "RIGHT", 4, 0)
    row.nameText:SetPoint("RIGHT", -60, 0)
    row.nameText:SetJustifyH("LEFT")
    row.nameText:SetWordWrap(false)

    row.sourceText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.sourceText:SetPoint("RIGHT", -4, 0)
    row.sourceText:SetJustifyH("RIGHT")

    row.highlight = row:CreateTexture(nil, "HIGHLIGHT")
    row.highlight:SetAllPoints()
    row.highlight:SetColorTexture(1, 1, 1, 0.08)

    row.selected = row:CreateTexture(nil, "BACKGROUND")
    row.selected:SetAllPoints()
    row.selected:SetColorTexture(0.2, 0.6, 0.8, 0.15)
    row.selected:Hide()
end

-- Subcategory button pool (for thematic styles)
local subcatButtons = {}

-- Source-type filter buttons (for thematic styles, replaces room buttons)
local sourceButtons = {}

local SOURCE_FILTER_DEFS = {
    { key = nil,         label = "All" },
    { key = 5,           label = "Vendor" },
    { key = 6,           label = "Craft" },
    { key = 2,           label = "Quest" },
    { key = 3,           label = "WQ" },
    { key = 4,           label = "Drop" },
    { key = 1,           label = "Achieve" },
    { key = 10,          label = "Promo" },
    { key = 9,           label = "Treasure" },
    { key = "rep",       label = "Rep" },
    { key = "endeavor",  label = "Endeavor" },
}

-- Cross-reference filters (pre-computed in StyleEngine itemCache at build time)
local function ItemHasRepRequirement(itemID)
    local ic = StyleEngine and StyleEngine:GetItemInfo(itemID)
    return ic and ic.hasRepReq or false
end

local function ItemCostsEndeavor(itemID)
    local ic = StyleEngine and StyleEngine:GetItemInfo(itemID)
    return ic and ic.costsEndeavor or false
end

-- Lookup style def from engine's merged table (built-in + custom)
local function GetStyleDef(styleId)
    if StyleEngine and StyleEngine.GetAllStyleDefs then
        local defs = StyleEngine:GetAllStyleDefs()
        return defs and defs[styleId]
    end
    return nil
end

-- Gallery cell pool
local cardGridFrame, cardGridScrollBox, cardGridScrollBar
-- Card interaction handlers (forward declarations; assigned below their definitions)
local StyleCell_OnEnter, StyleCell_OnLeave, StyleCell_OnClick

-- ============================================================================
-- INIT / LIFECYCLE
-- ============================================================================

function StylesTab:Init(cfg, mf, pdb)
    CONFIG = cfg or privateDB.CONFIG
    mainFrame = mf
    StyleEngine = privateDB.HDG_StyleEngine
    StyleSerializer = privateDB.HDG_StyleSerializer
    StylesTab.initialized = true
    StylesTab._config = CONFIG
    StylesTab._styleEngine = StyleEngine

    -- Load persisted collapse state, defaulting all to collapsed
    if HDG_Settings and type(HDG_Settings.stylesCollapsed) == "table" and HDG_Settings.stylesCollapsed._initialized then
        for k, v in pairs(HDG_Settings.stylesCollapsed) do
            if k ~= "_initialized" then collapsedTiers[k] = v end
        end
    else
        -- First time: default all sections to collapsed
        collapsedTiers["_shoppinglist"] = true
        collapsedTiers["collection"] = true
        if HDG_StyleTiers then
            for _, ti in ipairs(HDG_StyleTiers) do
                collapsedTiers[ti.key] = true
            end
        end
        if HDG_Settings then
            local saved = { _initialized = true }
            for k, v in pairs(collapsedTiers) do saved[k] = v end
            HDG_Settings.stylesCollapsed = saved
        end
    end

    -- Re-render when the catalog searcher finishes. RefreshAll runs at window
    -- open and kicks off an async catalog search; this listener picks up the
    -- result and re-renders so the visible view updates with fresh stats.
    -- Mirrors the equivalent listener in HDG_RecipesTab.
    if HDG.EventBus then
        HDG.EventBus:Register("HDG_COLLECTION_READY", function()
            statsCacheDirty = true
            if panel and panel:IsShown() then StylesTab:Refresh() end
        end)
    end
end

function StylesTab:SetSelectedStyle(v) selectedStyle = v end

function StylesTab:Show()
    local HDG_Debug = privateDB.HDG_Debug
    local Timed = (HDG_Debug and HDG_Debug.Timed) or function(_, fn) return fn() end

    if not panel then Timed("Styles.Create", function() self:Create() end) end
    -- Ensure cache is built on first show
    if StyleEngine and not StyleEngine:IsCacheReady() then
        Timed("Styles.BuildCache", function() StyleEngine:BuildCache() end)
    end
    if selectedShoppingListId then
        Timed("Styles.ShowShoppingListDetail", function() self:ShowShoppingListDetail(selectedShoppingListId) end)
    elseif selectedSessionData then
        Timed("Styles.ShowSessionDetail", function()
            self:ShowSessionDetail(selectedSessionData.houseGUID, selectedSessionData.sessionID, selectedSessionData.displayName)
        end)
    elseif selectedStyle then
        Timed("Styles.ShowDetail", function() self:ShowDetail(selectedStyle) end)
    else
        Timed("Styles.ShowLanding", function() self:ShowLanding() end)
    end
    panel:Show()
end

function StylesTab:Hide()
    if panel then panel:Hide() end
end

function StylesTab:Refresh()
    if not panel or not panel:IsShown() then return end
    if selectedShoppingListId then
        self:RefreshShoppingListDetail()
    elseif selectedSessionData then
        self:RefreshSessionDetail()
    elseif selectedStyle then
        self:RefreshDetail()
    else
        self:RefreshLanding()
    end
end

-- ============================================================================
-- CREATE (called once)
-- ============================================================================

function StylesTab:Create()
    local scheme = GetScheme()

    -- Main panel using TabPanel factory
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })
    panel = layout.frame
    panel:Hide()

    -- Landing frame (card grid)
    landingFrame = CreateFrame("Frame", nil, panel)
    landingFrame:SetAllPoints()

    -- Detail frame (style browser)
    detailFrame = CreateFrame("Frame", nil, panel)
    detailFrame:SetAllPoints()
    detailFrame:Hide()

    StylesTab._panel = panel
    StylesTab._landingFrame = landingFrame
    StylesTab._detailFrame = detailFrame

    self:CreateLanding()
    self:CreateDetail()
end

-- ============================================================================
-- LANDING PAGE (style card grid)
-- ============================================================================

local landingSearchText = ""
local landingSearchTimer = nil
local landingScrollBox, landingScrollBar, landingScrollView
local landingCatButtons = {}

local LANDING_CAT_DEFS = {
    { key = nil,            label = "All" },
    { key = "shoppinglist", label = "Shopping" },
    { key = "thematic",     label = "Thematic" },
    { key = "collection",   label = "Collections" },
    { key = "custom",       label = "My Styles" },
    { key = "snapshot",     label = "Snapshots" },
}

-- Forward declaration (defined after CreateLanding)
local ConstructLandingRow

-- ============================================================================
-- SHOPPING LIST CARD POOL
-- ============================================================================

-- (Shopping list cards reuse the row pool pattern)

function StylesTab:CreateLanding()
    local scheme = GetScheme()

    -- Header
    local header = landingFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    header:SetPoint("TOPLEFT", 10, -8)
    if scheme then ApplyFont(header, scheme, "header") end
    header:SetText(L["STYLE_BROWSE_HEADER"] .. " |cFF888888" .. L["STYLE_BETA"] .. "|r")
    landingFrame.header = header

    -- Style Creator button (top-right)
    local creatorBtn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
    creatorBtn.text:SetText(L["STYLE_CREATOR"])
    creatorBtn:SetWidth(creatorBtn.text:GetStringWidth() + 20)
    creatorBtn:SetPoint("TOPRIGHT", -28, -8)
    creatorBtn:SetScript("OnClick", function()
        StylesTab:ShowEditor(nil)
    end)
    landingFrame.creatorBtn = creatorBtn

    -- Import button (next to creator)
    local importBtn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
    importBtn.text:SetText(L["STYLE_IMPORT"])
    importBtn:SetWidth(importBtn.text:GetStringWidth() + 16)
    importBtn:SetPoint("RIGHT", creatorBtn, "LEFT", -4, 0)
    importBtn:SetScript("OnClick", function()
        StylesTab:ShowImportDialog()
    end)
    landingFrame.importBtn = importBtn

    -- Snapshot button (captures all placed decor account-wide)
    local snapshotBtn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
    snapshotBtn.text:SetText(L["STYLE_SAVE_PLACED_DECOR"])
    snapshotBtn:SetWidth(snapshotBtn.text:GetStringWidth() + 20)
    snapshotBtn:SetPoint("RIGHT", importBtn, "LEFT", -4, 0)
    snapshotBtn:SetScript("OnClick", function()
        StylesTab:TakeSnapshot()
    end)
    landingFrame.snapshotBtn = snapshotBtn

    -- Category switcher tabs
    local catX = 10
    for _, cd in ipairs(LANDING_CAT_DEFS) do
        local btn = HDG_UIHelpers.CreateChipButton(landingFrame, "", 60, false, nil)
        btn.text:SetText(cd.label)
        btn:SetWidth(btn.text:GetStringWidth() + 16)
        btn:SetPoint("TOPLEFT", catX, -30)
        btn.catKey = cd.key
        btn:SetScript("OnClick", function()
            selectedLandingCategory = cd.key
            for _, b in ipairs(landingCatButtons) do
                HDG_UIHelpers.UpdateChipButtonState(b, b.catKey == selectedLandingCategory)
            end
            self:RefreshLanding()
        end)
        HDG_UIHelpers.UpdateChipButtonState(btn, cd.key == selectedLandingCategory)
        landingCatButtons[#landingCatButtons + 1] = btn
        catX = catX + btn:GetWidth() + 2
    end

    -- Modern WowScrollBoxList for landing tier rows. Virtualization is native
    -- (only viewport frames exist). Header rows and tier rows are interleaved
    -- in the same DataProvider; the initializer dispatches on elementData.type.
    landingScrollBox = CreateFrame("Frame", nil, landingFrame, "WowScrollBoxList")
    landingScrollBox:SetPoint("TOPLEFT", 4, -56)
    landingScrollBox:SetPoint("BOTTOMRIGHT", -24, 4)

    landingScrollBar = CreateFrame("EventFrame", nil, landingFrame, "MinimalScrollBar")
    landingScrollBar:SetPoint("TOPLEFT", landingScrollBox, "TOPRIGHT", 2, 0)
    landingScrollBar:SetPoint("BOTTOMLEFT", landingScrollBox, "BOTTOMRIGHT", 2, 0)

    landingScrollView = CreateScrollBoxListLinearView(0, 0, 0, 0, 2)
    landingScrollView:SetElementExtentCalculator(function(_, elementData)
        if elementData.type == "header" then return ROW_HEIGHT_TIER_HEADER or 22 end
        return ROW_HEIGHT_LANDING or 43
    end)
    landingScrollView:SetElementInitializer("Button", function(frame, elementData)
        StylesTab:_RenderLandingElement(frame, elementData)
    end)

    ScrollUtil.InitScrollBoxListWithScrollBar(landingScrollBox, landingScrollBar, landingScrollView)

    -- Landing search box
    local searchBox = CreateFrame("EditBox", nil, landingFrame, "SearchBoxTemplate")
    searchBox:SetSize(150, 20)
    searchBox:SetPoint("TOPRIGHT", -28, -30)
    searchBox:SetAutoFocus(false)
    searchBox:HookScript("OnTextChanged", function(self)
        local text = self:GetText() or ""
        if text == landingSearchText then return end
        landingSearchText = text
        if landingSearchTimer then landingSearchTimer:Cancel() end
        landingSearchTimer = C_Timer.NewTimer(0.2, function()
            landingSearchTimer = nil
            StylesTab:RefreshLanding()
        end)
    end)
    landingFrame.searchBox = searchBox

    -- Collection count (left of search box)
    landingFrame.collectionCount = landingFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    landingFrame.collectionCount:SetPoint("RIGHT", searchBox, "LEFT", -8, 0)
    landingFrame.collectionCount:SetJustifyH("RIGHT")
    landingFrame.collectionCount:SetShadowOffset(1, -1)
    landingFrame.collectionCount:SetShadowColor(0, 0, 0, 0.8)

end

-- ============================================================================
-- LANDING ROW HANDLERS (module-scope, no per-row closures)
-- ============================================================================

local function StyleRow_OnClick(self)
    local rd = self._rowData
    if not rd then return end
    if rd.isRecentSession then
        StylesTab:ShowSessionDetail(rd.houseGUID, rd.sessionID, rd.displayName)
        return
    end
    if rd.tier == "_shoppinglist" then
        StylesTab:ShowShoppingListDetail(rd.listId)
    else
        StylesTab:ShowDetail(rd.styleId)
    end
end

local function StyleRow_OnEnter(self)
    if self.glowTex then self.glowTex:Show() end
end

local function StyleRow_OnLeave(self)
    if self.glowTex then self.glowTex:Hide() end
    GameTooltip:Hide()
end

-- ============================================================================
-- LANDING ROW FACTORY + POPULATE
-- ============================================================================

-- ROW_HEIGHT_LANDING is now declared at module scope above ConstructItemListRow
-- so the ExtentCalculator closure sees it as an upvalue.

ConstructLandingRow = function(parent)
    -- ScrollBoxList passes the pool-allocated frame as `parent`. Populate it
    -- in place rather than creating a child — children would orphan the
    -- ScrollBox's anchoring + click handling. Height is managed by the view's
    -- extentCalculator; no SetHeight/SetPoint needed here.
    local row = parent

    -- Row background — wood sign atlas (same as tab bar, stretches horizontally)
    row.bgTex = row:CreateTexture(nil, "BACKGROUND", nil, 1)
    row.bgTex:SetPoint("TOPLEFT", 0, 0)
    row.bgTex:SetPoint("BOTTOMRIGHT", 0, 0)
    row.bgTex:SetAtlas("housing-woodsign")
    row.bgTex:SetAlpha(0.5)

    -- Hover glow (brighter wood)
    row.glowTex = row:CreateTexture(nil, "BACKGROUND", nil, 2)
    row.glowTex:SetPoint("TOPLEFT", 0, 0)
    row.glowTex:SetPoint("BOTTOMRIGHT", 0, 0)
    row.glowTex:SetAtlas("housing-woodsign")
    row.glowTex:SetAlpha(0.3)
    row.glowTex:Hide()

    -- Icon (36x36)
    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(36, 36)
    row.icon:SetPoint("TOPLEFT", 6, -4)

    -- Name text (flex width, stops 300px from right edge)
    row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.nameText:SetPoint("TOPLEFT", row.icon, "TOPRIGHT", 8, -1)
    row.nameText:SetPoint("RIGHT", row, "RIGHT", -300, 0)
    row.nameText:SetJustifyH("LEFT")
    row.nameText:SetWordWrap(false)
    row.nameText:SetShadowOffset(1, -1)
    row.nameText:SetShadowColor(0, 0, 0, 0.8)

    -- Description text (dim, below name)
    row.descText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    -- URL link icon (shown left of description for rows with a URL)
    row.urlIcon = row:CreateTexture(nil, "OVERLAY")
    row.urlIcon:SetSize(12, 12)
    row.urlIcon:SetPoint("TOPLEFT", row.nameText, "BOTTOMLEFT", 0, -2)
    row.urlIcon:SetTexture("Interface\\AddOns\\HousingDecorGuide\\textures\\url_link")
    row.urlIcon:Hide()

    row.descText:SetPoint("TOPLEFT", row.urlIcon, "TOPRIGHT", 2, 1)
    row.descText:SetPoint("RIGHT", row.nameText, "RIGHT", 0, 0)
    row.descText:SetJustifyH("LEFT")
    row.descText:SetWordWrap(false)
    row.descText:SetMaxLines(1)
    row.descText:SetShadowOffset(1, -1)
    row.descText:SetShadowColor(0, 0, 0, 0.6)

    -- Inline EditBox for URL copy (replaces descText on click, hidden by default)
    row.urlEditBox = CreateFrame("EditBox", nil, row)
    row.urlEditBox:SetPoint("TOPLEFT", row.urlIcon, "TOPRIGHT", 2, 1)
    row.urlEditBox:SetPoint("RIGHT", row.nameText, "RIGHT", 0, 0)
    row.urlEditBox:SetHeight(14)
    row.urlEditBox:SetFontObject("GameFontHighlightExtraSmall")
    row.urlEditBox:SetAutoFocus(false)
    row.urlEditBox:EnableMouse(true)
    row.urlEditBox:Hide()
    row.urlEditBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
        self:Hide()
        row.descText:Show()
    end)
    row.urlEditBox:SetScript("OnEditFocusLost", function(self)
        self:Hide()
        row.descText:Show()
    end)

    -- Clickable overlay for URL (covers icon + desc area)
    row.urlBtn = CreateFrame("Button", nil, row)
    row.urlBtn:SetPoint("TOPLEFT", row.urlIcon, "TOPLEFT", 0, 0)
    row.urlBtn:SetPoint("BOTTOMRIGHT", row.descText, "BOTTOMRIGHT", 0, 0)
    row.urlBtn:SetFrameLevel(row:GetFrameLevel() + 3)
    row.urlBtn:SetScript("OnEnter", function(self)
        if self._url then
            GameTooltip:SetOwner(self, "ANCHOR_TOP")
            GameTooltip:AddLine("Click to copy URL", 1, 1, 1)
            GameTooltip:Show()
            row.descText:SetTextColor(0.4, 0.8, 1, 1)
        end
    end)
    row.urlBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
        local s = row._scheme
        if s then row.descText:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, 0.8) end
    end)
    row.urlBtn:SetScript("OnClick", function(self)
        if not self._url then return end
        row.descText:Hide()
        row.urlEditBox:SetText(self._url)
        row.urlEditBox:Show()
        row.urlEditBox:SetFocus()
        row.urlEditBox:HighlightText()
    end)
    row.urlBtn:Hide()

    -- Preview icons (6 x 24x24, anchored from right, 2px gaps)
    -- Total width: 6*24 + 5*2 = 154px. Start at -188 from right (leaves room for count)
    row.previewIcons = {}
    for i = 1, 6 do
        local tex = row:CreateTexture(nil, "ARTWORK")
        tex:SetSize(24, 24)
        if i == 1 then
            tex:SetPoint("RIGHT", row, "RIGHT", -188, 0)
        else
            tex:SetPoint("LEFT", row.previewIcons[i - 1], "RIGHT", 2, 0)
        end
        tex:Hide()
        row.previewIcons[i] = tex
    end

    -- Count text (right of preview icons area)
    row.countText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.countText:SetWidth(46)
    row.countText:SetPoint("RIGHT", row.previewIcons[1], "LEFT", -8, 0)
    row.countText:SetJustifyH("RIGHT")
    row.countText:SetShadowOffset(1, -1)
    row.countText:SetShadowColor(0, 0, 0, 0.8)

    -- Progress bar (journeys renown style, aligned under count text)
    row.progressBg = row:CreateTexture(nil, "ARTWORK")
    row.progressBg:SetHeight(8)
    row.progressBg:SetPoint("BOTTOMLEFT", row.countText, "BOTTOMLEFT", -2, -8)
    row.progressBg:SetPoint("BOTTOMRIGHT", row.countText, "BOTTOMRIGHT", 2, -8)
    row.progressBg:SetAtlas("UI-Journeys-renown-progressbar-BG")

    row.progressFill = row:CreateTexture(nil, "OVERLAY")
    row.progressFill:SetHeight(8)
    row.progressFill:SetPoint("BOTTOMLEFT", row.progressBg, "BOTTOMLEFT", 0, 0)
    row.progressFill:SetAtlas("UI-Journeys-renown-progressbar-fill")
    row.progressFill:SetVertexColor(0.2, 0.8, 0.6, 1)

    row.progressFrame = row:CreateTexture(nil, "OVERLAY", nil, 1)
    row.progressFrame:SetHeight(8)
    row.progressFrame:SetAllPoints(row.progressBg)
    row.progressFrame:SetAtlas("UI-Journeys-renown-progressbar-frame")

    -- Action buttons (hidden, shown on hover, overlaying right side)
    -- Action buttons: 2x2 grid anchored to right edge, always visible when active
    -- Layout:  [C] [E]
    --          [X] [D]
    local actionDefs = {
        { key = "copyEdit", label = "C", tip = "Copy + Edit", col = 1, gridRow = 1 },
        { key = "edit",     label = "E", tip = "Edit",        col = 2, gridRow = 1 },
        { key = "export",   label = "X", tip = "Export",      col = 1, gridRow = 2 },
        { key = "delete",   label = "D", tip = "Delete",      col = 2, gridRow = 2 },
    }
    row._actionBtns = {}
    local btnW, btnH, btnGap = 20, 18, 2
    for _, ad in ipairs(actionDefs) do
        local ab = HDG_UIHelpers.CreateChipButton(row, ad.label, btnW, false, nil)
        ab:SetSize(btnW, btnH)
        ab:ClearAllPoints()
        local xOff = -(4 + (2 - ad.col) * (btnW + btnGap))
        local yOff = ad.gridRow == 1 and -(4) or -(4 + btnH + btnGap)
        ab:SetPoint("TOPRIGHT", row, "TOPRIGHT", xOff, yOff)
        ab:SetFrameLevel(row:GetFrameLevel() + 5)
        ab:SetScript("OnEnter", function(self)
            self.highlight:Show()
            GameTooltip:SetOwner(self, "ANCHOR_TOP")
            GameTooltip:AddLine(ad.tip, 1, 1, 1)
            GameTooltip:Show()
        end)
        ab:SetScript("OnLeave", function(self)
            self.highlight:Hide()
            GameTooltip:Hide()
        end)
        ab:Hide()
        ab._active = false
        row._actionBtns[ad.key] = ab
    end

    row:SetScript("OnClick", StyleRow_OnClick)
    row:SetScript("OnEnter", StyleRow_OnEnter)
    row:SetScript("OnLeave", StyleRow_OnLeave)

    -- Track per-row widgets so they can be hidden when showing the header layer.
    -- bgTex/glowTex are deliberately omitted: bgTex is the wood background that
    -- should also paint the header rows; glowTex is hover state and stays under
    -- StyleRow_OnEnter/OnLeave control which works fine for both row + header.
    row._rowWidgets = {
        row.icon, row.nameText, row.descText,
        row.urlIcon, row.urlEditBox, row.urlBtn, row.countText,
        row.progressBg, row.progressFill, row.progressFrame,
    }
    for _, tex in ipairs(row.previewIcons) do
        table.insert(row._rowWidgets, tex)
    end
    for _, ab in pairs(row._actionBtns) do
        table.insert(row._rowWidgets, ab)
    end

    -- Header layer: hidden by default; shown when elementData.type == "header"
    local headerLayer = CreateFrame("Frame", nil, row)
    headerLayer:SetAllPoints()
    local headerLabel = headerLayer:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    headerLabel:SetPoint("LEFT", 8, 0)
    headerLabel:SetTextColor(1, 0.82, 0)
    headerLayer.label = headerLabel

    local headerCount = headerLayer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    headerCount:SetPoint("RIGHT", -8, 0)
    headerCount:SetTextColor(0.7, 0.7, 0.7)
    headerLayer.count = headerCount

    headerLayer:Hide()
    row._headerLayer = headerLayer

    return row
end

local function UpdateStyleRow(row, rowData, scheme)
    row._rowData = rowData
    row._scheme = scheme

    -- Icon — shopping lists use their explicit icon (wowhead logo or bag);
    -- everything else uses the first preview item's decor icon
    if rowData.tier == "_shoppinglist" then
        if rowData.icon then
            row.icon:SetTexture(rowData.icon)
        elseif rowData.iconAtlas then
            row.icon:SetAtlas(rowData.iconAtlas)
        else
            row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        end
    else
        local previews = rowData.previewIcons or {}
        local firstPick = previews[1]
        local ic = firstPick and StyleEngine and StyleEngine:GetItemInfo(firstPick.itemID)
        if ic and ic.iconAtlas then
            row.icon:SetAtlas(ic.iconAtlas)
        elseif ic and ic.iconTexture then
            row.icon:SetTexture(ic.iconTexture)
        elseif rowData.iconAtlas then
            row.icon:SetAtlas(rowData.iconAtlas)
        elseif rowData.icon then
            row.icon:SetTexture(rowData.icon)
        else
            row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        end
    end

    -- Name (lighten accent color so it's always readable on dark wood)
    row.nameText:SetText(rowData.displayName or "")
    if rowData.color then
        local c = rowData.color
        local lift = 0.4 -- blend 40% towards white
        row.nameText:SetTextColor(c.r + (1 - c.r) * lift, c.g + (1 - c.g) * lift, c.b + (1 - c.b) * lift, 1)
    elseif scheme then
        row.nameText:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, 1)
    end
    if scheme then ApplyFont(row.nameText, scheme, "normal") end

    -- Description + URL
    local desc = rowData.description or ""
    row.descText:SetText(desc)
    row.descText:SetShown(desc ~= "")
    local hasUrl = rowData.url and rowData.url ~= ""
    row.urlIcon:SetShown(hasUrl and desc ~= "")
    row.urlBtn._url = hasUrl and rowData.url or nil
    row.urlBtn:SetShown(hasUrl and desc ~= "")
    -- Pool rebind hygiene: if the prior owner of this row clicked to copy, the
    -- EditBox was left visible with their URL. Reset on every bind so recycled
    -- rows always start as static text, not EditBox.
    if row.urlEditBox:IsShown() then
        row.urlEditBox:ClearFocus()
        row.urlEditBox:Hide()
    end
    -- Re-anchor desc: if URL icon visible, offset from icon; else from name
    row.descText:ClearAllPoints()
    if hasUrl and desc ~= "" then
        row.descText:SetPoint("TOPLEFT", row.urlIcon, "TOPRIGHT", 2, 1)
    else
        row.descText:SetPoint("TOPLEFT", row.nameText, "BOTTOMLEFT", 0, -1)
    end
    row.descText:SetPoint("RIGHT", row.nameText, "RIGHT", 0, 0)
    if scheme then
        row.descText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 0.8)
    end

    -- Preview icons
    local previews = rowData.previewIcons or {}
    for i = 1, 6 do
        local tex = row.previewIcons[i]
        local pick = previews[i]
        if pick then
            local ic = pick.itemID and StyleEngine and StyleEngine:GetItemInfo(pick.itemID)
            local hasIcon = false
            if ic and ic.iconAtlas then
                tex:SetAtlas(ic.iconAtlas)
                hasIcon = true
            elseif ic and ic.iconTexture then
                tex:SetAtlas("") -- clear any prior atlas
                tex:SetTexture(ic.iconTexture)
                hasIcon = true
            elseif pick.iconID then
                tex:SetAtlas("") -- clear any prior atlas
                tex:SetTexture(pick.iconID)
                hasIcon = true
            end
            if not hasIcon then
                tex:Hide()
            else
                tex:SetDesaturated(not pick.isCollected)
                tex:SetAlpha(pick.isCollected and 1 or 0.5)
                tex:Show()
            end
        else
            tex:Hide()
        end
    end

    -- Count text
    local collected = rowData.collected or 0
    local total = rowData.total or 0
    if rowData.isSnapshot then
        row.countText:SetText(total .. " placed")
    elseif rowData.tier == "_shoppinglist" then
        row.countText:SetText(total .. " items")
    else
        row.countText:SetText(collected .. "/" .. total)
    end
    if scheme then
        ApplyFont(row.countText, scheme, "small")
        row.countText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
    end

    -- Progress bar
    local pct = (total > 0 and not rowData.isSnapshot) and (collected / total) or 0
    row.progressFill:SetWidth(math.max(1, (row.countText:GetWidth() or 46) * pct))
    row.progressFill:SetShown(pct > 0)
    row.progressBg:SetShown(not rowData.isSnapshot and rowData.tier ~= "_shoppinglist")
    if rowData.color then
        row.progressFill:SetVertexColor(rowData.color.r, rowData.color.g, rowData.color.b, 1)
    end

    -- Completed state
    local isCompleted = (pct >= 1.0 and total > 0)
    -- Completed: show glow permanently + gold progress bar
    if row.glowTex then row.glowTex:SetShown(isCompleted) end
    if isCompleted then
        row.progressFill:SetVertexColor(1, 0.84, 0, 1)
    end

    -- Action buttons: show active ones, hide inactive
    local btns = row._actionBtns
    btns.copyEdit._active = rowData.canCopyEdit or false
    btns.edit._active = rowData.canEdit or false
    btns.export._active = rowData.canExport or false
    btns.delete._active = rowData.canDelete or false
    for _, ab in pairs(btns) do ab:SetShown(ab._active) end

    -- Wire action button OnClick
    if rowData.canCopyEdit then
        btns.copyEdit:SetScript("OnClick", function()
            local def = GetStyleDef(rowData.styleId)
            if def then
                local dupDef = StylesTab.DeepCopyTable(def)
                dupDef.displayName = (dupDef.displayName or "") .. " (Copy)"
                StylesTab:ShowEditor(nil, dupDef)
            end
        end)
    end
    if rowData.canEdit then
        btns.edit:SetScript("OnClick", function()
            StylesTab:ShowEditor(rowData.styleId)
        end)
    end
    if rowData.canExport then
        btns.export:SetScript("OnClick", function()
            if not StyleSerializer then return end
            local def = GetStyleDef(rowData.styleId)
            if def then
                local encoded
                if rowData.isSnapshot then
                    encoded = StyleSerializer:ExportSnapshot(def)
                else
                    encoded = StyleSerializer:Export(def)
                end
                if encoded then StylesTab:ShowExportDialog(encoded, rowData.isSnapshot and def or nil) end
            end
        end)
    end
    if rowData.canDelete then
        btns.delete:SetScript("OnClick", function()
            StaticPopup_Show("HDG_DELETE_CUSTOM_STYLE", rowData.displayName or rowData.styleId, nil, { key = rowData.styleId })
        end)
    end

    -- Reset hover state
    if row.glowTex then row.glowTex:Hide() end
end

-- ============================================================================
-- RECENT SESSION ROW BUILDER
-- ============================================================================

local function buildRecentSessionRows(passFilter)
    local rows = {}
    if not (HDG.RecentActivity and HDG.RecentActivity.GetSessions) then return rows end
    if not (C_Housing and C_Housing.GetCurrentHouseInfo) then return rows end
    local houseInfo = C_Housing.GetCurrentHouseInfo()
    local houseGUID = houseInfo and houseInfo.houseGUID
    if not houseGUID then return rows end

    local sessions = HDG.RecentActivity:GetSessions(houseGUID)
    for _, s in ipairs(sessions) do
        local label
        if s.isActive then
            label = "Edit session - Now"
        else
            -- date() returns string when the format string doesn't begin with
            -- "*t"; LuaLS can't narrow that at the type level, so cast.
            local stamp = date("%b %d %I:%M %p", s.endedAt or s.startedAt)
            ---@cast stamp string
            label = "Edit session - " .. stamp:gsub(" 0", " ")
        end
        if passFilter(label, "snapshot") then
            table.insert(rows, {
                styleId      = "_session_" .. s.sessionID,
                displayName  = label,
                iconAtlas    = s.isActive and "communities-chat-icon-plus" or "communities-icon-clock",
                color        = { r = 0.6, g = 0.7, b = 0.9 },
                description  = s.eventCount .. " events",
                tier         = "snapshot",
                tierLabel    = "Recent Edit Sessions / House Snapshots",
                collected    = s.eventCount,
                total        = s.eventCount,
                isSnapshot   = false,
                isRecentSession = true,
                sessionID    = s.sessionID,
                houseGUID    = houseGUID,
                canCopyEdit  = false, canEdit = false, canExport = false, canDelete = false,
                previewIcons = (function()
                    local previews = {}
                    if HDG.RecentActivity and HDG.RecentActivity.GetEventGroups then
                        local groups = HDG.RecentActivity:GetEventGroups(houseGUID, s.sessionID)
                        for i = 1, math.min(6, #groups) do
                            local g = groups[i]
                            table.insert(previews, {
                                iconID      = g.icon,
                                isCollected = (g.eventType == "placed" and g.liveCount > 0),
                            })
                        end
                    end
                    return previews
                end)(),
            })
        end
    end
    return rows
end

-- ============================================================================
-- LANDING DATA BUILDER
-- ============================================================================

local function BuildLandingData()
    local rows = {}
    local searchLower = landingSearchText ~= "" and landingSearchText:lower() or nil
    local catFilter = selectedLandingCategory

    local function passFilter(displayName, tier)
        if searchLower and not displayName:lower():find(searchLower, 1, true) then return false end
        if catFilter then
            if catFilter == "shoppinglist" and tier ~= "_shoppinglist" then return false end
            if catFilter == "collection" and tier ~= "collection" then return false end
            if catFilter == "thematic" and tier ~= "thematic" and tier ~= "themed" and tier ~= "faction" then return false end
            if catFilter == "custom" and tier ~= "custom" and tier ~= "curated" then return false end
            if catFilter == "snapshot" and tier ~= "snapshot" then return false end
        end
        return true
    end

    local function buildPreviewIcons(styleId, def)
        if not StyleEngine then return {} end
        local items = StyleEngine:GetStyleItems(styleId)
        if not items then return {} end
        local bucket
        if def and def.tier == "snapshot" and items.items then
            bucket = items.items
        elseif items.curated then
            bucket = items.curated
        elseif def and def.subcategories and def.subcategories[1] then
            bucket = items[def.subcategories[1].key]
        elseif items.first then
            bucket = items.first
        end
        if not bucket then return {} end

        local coll, uncoll = {}, {}
        for _, item in ipairs(bucket) do
            if HDG_Data and HDG_Data.IsItemCollected(item.itemID) then
                if #coll < 6 then coll[#coll + 1] = { itemID = item.itemID, decorID = item.decorID, isCollected = true } end
            else
                if #uncoll < 6 then uncoll[#uncoll + 1] = { itemID = item.itemID, decorID = item.decorID, isCollected = false } end
            end
            if #coll >= 6 then break end
        end
        local picks = {}
        for _, p in ipairs(coll) do picks[#picks + 1] = p; if #picks >= 6 then break end end
        if #picks < 6 then
            for _, p in ipairs(uncoll) do picks[#picks + 1] = p; if #picks >= 6 then break end end
        end
        return picks
    end

    -- Helper: add custom/curated styles for a tier key (sorted alphabetically)
    local function addCustomStyles(tierKey, tierLabel)
        local customStyles, customOrder = HDG.Store:GetCustomStyles()
        if not customOrder then return end
        local startIdx = #rows + 1
        for _, styleId in ipairs(customOrder) do
            local def = customStyles[styleId]
            if def and (def.tier == tierKey or (tierKey == "custom" and def.tier == "curated")) then
                if passFilter(def.displayName, def.tier or tierKey) then
                    local st = statsCache[styleId] or { collected = 0, total = 0 }
                    local isSnap = (def.tier == "snapshot")
                    rows[#rows + 1] = {
                        styleId = styleId,
                        displayName = def.displayName,
                        icon = def.icon,
                        iconAtlas = def.iconAtlas or (isSnap and "decor-controls-decoratemode-active" or nil),
                        color = def.color or { r = 0.7, g = 0.7, b = 0.7 },
                        description = def.description or "",
                        tier = tierKey, -- normalize curated → custom so they share one header
                        tierLabel = tierLabel,
                        collected = st.collected,
                        total = st.total,
                        isSnapshot = isSnap,
                        canCopyEdit = not isSnap,
                        canEdit = not isSnap,
                        canExport = true,
                        canDelete = true,
                        previewIcons = buildPreviewIcons(styleId, def),
                    }
                end
            end
        end
        -- Sort this section alphabetically by name
        local endIdx = #rows
        if endIdx > startIdx then
            local section = {}
            for i = startIdx, endIdx do section[#section + 1] = rows[i] end
            table.sort(section, function(a, b) return (a.displayName or ""):lower() < (b.displayName or ""):lower() end)
            for i, rd in ipairs(section) do rows[startIdx + i - 1] = rd end
        end
    end

    -- Helper: add built-in styles for a tier key
    local function addBuiltinStyles(tierKey, tierLabel)
        if not HDG_StyleOrder or not HDG_StyleDefinitions then return end
        for _, styleId in ipairs(HDG_StyleOrder) do
            local def = HDG_StyleDefinitions[styleId]
            if def and def.tier == tierKey and passFilter(def.displayName, tierKey) then
                local st = statsCache[styleId] or { collected = 0, total = 0 }
                rows[#rows + 1] = {
                    styleId = styleId,
                    displayName = def.displayName,
                    icon = def.icon,
                    iconAtlas = def.iconAtlas,
                    color = def.color or { r = 0.7, g = 0.7, b = 0.7 },
                    description = def.description or "",
                    tier = tierKey,
                    tierLabel = tierLabel,
                    collected = st.collected,
                    total = st.total,
                    isSnapshot = false,
                    canCopyEdit = true, canEdit = false, canExport = false, canDelete = false,
                    previewIcons = buildPreviewIcons(styleId, def),
                }
            end
        end
    end

    -- 1. My Styles (custom/curated — user-created first)
    addCustomStyles("custom", "My Styles")

    -- 2. Shopping Lists
    local slLists = HDG.Store and HDG.Store:GetShoppingLists() or {}
    for _, listInfo in ipairs(slLists) do
        if passFilter(listInfo.name, "_shoppinglist") then
            local previews = {}
            local fullList = HDG.Store.state and HDG.Store.state.vendorShoppingLists and HDG.Store.state.vendorShoppingLists[listInfo.id]
            if fullList and fullList.items then
                for _, entry in ipairs(fullList.items) do
                    local itemID = entry.itemID or entry.id
                    if itemID and #previews < 6 then
                        previews[#previews + 1] = { itemID = itemID, isCollected = HDG_Data and HDG_Data.IsItemCollected(itemID) or false }
                    end
                end
            end
            local meta = fullList and fullList.meta
            local isWowhead = meta and meta.source == "wowhead"
            local slDesc = ""
            if isWowhead and meta.url then
                slDesc = meta.url
            elseif meta and meta.source then
                slDesc = meta.source:sub(1, 1):upper() .. meta.source:sub(2) .. " import"
            end
            rows[#rows + 1] = {
                styleId = "_sl_" .. listInfo.id,
                displayName = listInfo.name,
                icon = isWowhead and "Interface\\AddOns\\HousingDecorGuide\\textures\\wowhead_logo" or nil,
                iconAtlas = not isWowhead and "wowcodex-itemicon-bag" or nil,
                color = { r = 0.3, g = 0.8, b = 0.6 },
                description = slDesc,
                tier = "_shoppinglist",
                tierLabel = "Shopping Lists",
                collected = 0,
                total = listInfo.count or 0,
                isSnapshot = false,
                listId = listInfo.id,
                url = meta and meta.url or nil,
                canCopyEdit = false, canEdit = false, canExport = false, canDelete = false,
                previewIcons = previews,
            }
        end
    end

    -- 3. Snapshots
    addCustomStyles("snapshot", "Recent Edit Sessions / House Snapshots")

    -- 3b. Recent edit sessions interleaved under the same tier
    do
        local sessionRows = buildRecentSessionRows(passFilter)
        for _, r in ipairs(sessionRows) do
            rows[#rows + 1] = r
        end
    end

    -- 4. Built-in style tiers (Room Concepts, Themed, Faction)
    addBuiltinStyles("thematic", "Room Concepts")
    addBuiltinStyles("themed", "Themed")
    addBuiltinStyles("faction", "Faction")

    -- 5. Collections (last — pinned specials first, then alphabetical)
    if HDG_CollectionDefinitions and HDG_CollectionOrder then
        local pinned = { ["recently-learned"] = true, ["dyeable"] = true, ["trophies"] = true }
        local collStartIdx = #rows + 1
        local pinnedCount = 0
        -- Add pinned collections first (in order)
        for _, collectionId in ipairs(HDG_CollectionOrder) do
            if pinned[collectionId] then
                local def = HDG_CollectionDefinitions[collectionId]
                if def and passFilter(def.displayName, "collection") then
                    local st = statsCache[collectionId] or { collected = 0, total = 0 }
                    rows[#rows + 1] = {
                        styleId = collectionId, displayName = def.displayName,
                        icon = def.icon, iconAtlas = def.iconAtlas,
                        color = def.color or { r = 0.7, g = 0.7, b = 0.7 },
                        description = def.description or "",
                        tier = "collection", tierLabel = "Useful Collections",
                        collected = st.collected, total = st.total, isSnapshot = false,
                        canCopyEdit = false, canEdit = false, canExport = false, canDelete = false,
                        previewIcons = buildPreviewIcons(collectionId, def),
                    }
                    pinnedCount = pinnedCount + 1
                end
            end
        end
        -- Add remaining collections
        local restStartIdx = #rows + 1
        for _, collectionId in ipairs(HDG_CollectionOrder) do
            if not pinned[collectionId] then
                local def = HDG_CollectionDefinitions[collectionId]
                if def and passFilter(def.displayName, "collection") then
                    local st = statsCache[collectionId] or { collected = 0, total = 0 }
                    rows[#rows + 1] = {
                        styleId = collectionId, displayName = def.displayName,
                        icon = def.icon, iconAtlas = def.iconAtlas,
                        color = def.color or { r = 0.7, g = 0.7, b = 0.7 },
                        description = def.description or "",
                        tier = "collection", tierLabel = "Useful Collections",
                        collected = st.collected, total = st.total, isSnapshot = false,
                        canCopyEdit = false, canEdit = false, canExport = false, canDelete = false,
                        previewIcons = buildPreviewIcons(collectionId, def),
                    }
                end
            end
        end
        -- Sort non-pinned collections alphabetically
        local restEndIdx = #rows
        if restEndIdx > restStartIdx then
            local section = {}
            for i = restStartIdx, restEndIdx do section[#section + 1] = rows[i] end
            table.sort(section, function(a, b) return (a.displayName or ""):lower() < (b.displayName or ""):lower() end)
            for i, rd in ipairs(section) do rows[restStartIdx + i - 1] = rd end
        end
    end

    return rows
end

-- ============================================================================
-- REFRESH LANDING (WowScrollBoxList version)
-- ============================================================================

-- Render dispatcher for the landing ScrollBoxList. Called by the view's
-- element initializer for each visible frame. Mutates `frame` in place.
-- elementData has shape { type = "header"|"row", ... } -- see RefreshLanding.
function StylesTab:_RenderLandingElement(frame, elementData)
    frame:SetWidth(landingScrollBox:GetWidth())

    if not frame._hdgConstructed then
        ConstructLandingRow(frame)
        frame._hdgConstructed = true
    end

    if elementData.type == "row" then
        if frame._headerLayer then frame._headerLayer:Hide() end
        if frame._rowWidgets then
            for _, w in ipairs(frame._rowWidgets) do w:Show() end
        end
        if frame.bgTex then frame.bgTex:SetAlpha(0.5) end  -- standard row dimness
        UpdateStyleRow(frame, elementData.rowData, GetScheme())
        frame:SetScript("OnClick", StyleRow_OnClick)
        frame:EnableMouse(true)

    elseif elementData.type == "header" then
        if frame._rowWidgets then
            for _, w in ipairs(frame._rowWidgets) do w:Hide() end
        end
        if frame.bgTex then frame.bgTex:SetAlpha(0.95) end  -- brighter than content rows
        if frame._headerLayer then
            frame._headerLayer:Show()
            frame._headerLayer.label:SetText(elementData.tierLabel)
            frame._headerLayer.count:SetText(elementData.count .. " styles")
        end
        -- Click-to-toggle expand/collapse for this tier. State persists in
        -- HDG_Settings.stylesCollapsed; refresh re-runs RefreshLanding to
        -- show/hide the affected rows.
        frame._headerTier = elementData.tier
        frame:EnableMouse(true)
        frame:SetScript("OnClick", function(self)
            local tier = self._headerTier
            if not tier then return end
            collapsedTiers[tier] = not collapsedTiers[tier]
            if HDG_Settings then
                HDG_Settings.stylesCollapsed = HDG_Settings.stylesCollapsed or { _initialized = true }
                HDG_Settings.stylesCollapsed[tier] = collapsedTiers[tier]
                HDG_Settings.stylesCollapsed._initialized = true
            end
            StylesTab:RefreshLanding()
        end)
    end
end

function StylesTab:RefreshLanding()
    if not landingScrollBox then return end
    if not StyleEngine or not StyleEngine:IsCacheReady() then return end

    -- Warm collection cache if needed; defer render until warm completes
    if not HDG_Data.IsCollectionCacheWarmed() then
        HDG_Data.WarmCollectionCache(nil, function()
            statsCacheDirty = true
            StylesTab:RefreshLanding()
        end)
        return
    end

    -- Rebuild stats cache if dirty
    if statsCacheDirty then
        wipe(statsCache)
        for _, styleId in ipairs(HDG_StyleOrder) do
            statsCache[styleId] = StyleEngine:GetStyleCollectionStats(styleId, hideUnreleased)
        end
        if HDG_CollectionOrder then
            for _, cid in ipairs(HDG_CollectionOrder) do
                statsCache[cid] = StyleEngine:GetStyleCollectionStats(cid, hideUnreleased)
            end
        end
        local cs, co = HDG.Store:GetCustomStyles()
        if co then
            for _, sid in ipairs(co) do
                if not statsCache[sid] then
                    statsCache[sid] = StyleEngine:GetStyleCollectionStats(sid, hideUnreleased)
                end
            end
        end
        statsCacheDirty = false
    end

    -- Build flat data array. Headers are interleaved between tier groups
    -- when the "All" landing category is selected (showHeaders).
    local rows = BuildLandingData()
    local showHeaders = (selectedLandingCategory == nil)

    local tierCounts = {}
    for _, rd in ipairs(rows) do
        tierCounts[rd.tier] = (tierCounts[rd.tier] or 0) + 1
    end

    local elements = {}
    local lastTier = nil
    for _, rowData in ipairs(rows) do
        if showHeaders and rowData.tier ~= lastTier then
            table.insert(elements, {
                type      = "header",
                tier      = rowData.tier,
                tierLabel = rowData.tierLabel or rowData.tier,
                count     = tierCounts[rowData.tier] or 0,
            })
            lastTier = rowData.tier
        end
        if not (showHeaders and collapsedTiers[rowData.tier]) then
            table.insert(elements, { type = "row", rowData = rowData })
        end
    end

    local provider = CreateDataProvider(elements)
    landingScrollBox:SetDataProvider(provider, ScrollBoxConstants.RetainScrollPosition)

    -- Update collection count display (preserve exact formatting from prior implementation)
    if landingFrame.collectionCount then
        local totalDecor = 0
        local collectedDecor = 0
        if HDG_AllDecorDB then
            for itemID in pairs(HDG_AllDecorDB) do
                totalDecor = totalDecor + 1
                if HDG_Data and HDG_Data.IsItemCollected(itemID) then
                    collectedDecor = collectedDecor + 1
                end
            end
        end
        local pct = totalDecor > 0 and math.floor(collectedDecor / totalDecor * 100) or 0
        local scheme = GetScheme()
        local accentHex = scheme and HDG.Theme.C_Accent() or "|cFF14b8a6"
        landingFrame.collectionCount:SetText(
            accentHex .. collectedDecor .. "|r" ..
            HDG.Theme.C_Dim() .. "/" .. totalDecor .. " (" .. pct .. "%)|r"
        )
    end
end

-- ============================================================================
-- DETAIL VIEW (single style browser)
-- ============================================================================

local detailHeader, detailDesc, detailBackBtn
local tierButtons = {}
local filterBar, searchBox
local roomButtons = {}
local itemListScrollBox, itemListScrollBar, itemListScrollView
local detailPanel -- right-side item detail

function StylesTab:CreateDetail()
    local scheme = GetScheme()

    -- Back button
    detailBackBtn = CreateFrame("Button", nil, detailFrame, "UIPanelButtonTemplate")
    detailBackBtn:SetSize(60, 20)
    detailBackBtn:SetPoint("TOPLEFT", 6, -6)
    detailBackBtn:SetText(L["STYLE_BACK"])
    detailBackBtn:SetScript("OnClick", function()
        selectedStyle = nil
        StylesTab:ShowLanding()
    end)

    -- Style name header
    detailHeader = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    detailHeader:SetPoint("LEFT", detailBackBtn, "RIGHT", 8, 0)
    if scheme then ApplyFont(detailHeader, scheme, "header") end

    -- Description
    detailDesc = detailFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    detailDesc:SetPoint("TOPLEFT", 6, -30)
    detailDesc:SetPoint("RIGHT", -6, 0)
    detailDesc:SetJustifyH("LEFT")
    if scheme then
        ApplyFont(detailDesc, scheme, "xsmall")
        detailDesc:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
    end

    -- Tier sub-tabs
    local tierDefs = {
        { key = "first",   label = L["STYLE_TIER_SIGNATURE"] },
        { key = "second",  label = L["STYLE_TIER_ACCENT"] },
        { key = "neutral", label = L["STYLE_TIER_VERSATILE"] },
        { key = "clash",   label = L["STYLE_TIER_CLASHING"] },
    }
    local tierX = 6
    for _, td in ipairs(tierDefs) do
        local btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
        btn:SetSize(95, 22)
        btn:SetPoint("TOPLEFT", tierX, -48)

        btn.text:SetText(td.label)

        btn.tierKey = td.key
        btn:SetScript("OnClick", function()
            selectedTier = td.key
            selectedItemID = nil
            self:RefreshDetail()
        end)

        tierButtons[td.key] = btn
        tierX = tierX + 99
    end

    -- Filter bar
    filterBar = CreateFrame("Frame", nil, detailFrame)
    filterBar:SetPoint("TOPLEFT", 6, -74)
    filterBar:SetPoint("RIGHT", -6, 0)
    filterBar:SetHeight(22)

    -- Uncollected checkbox
    local uncollCheck = CreateFrame("CheckButton", nil, filterBar, "UICheckButtonTemplate")
    uncollCheck:SetSize(20, 20)
    uncollCheck:SetPoint("LEFT", 0, 0)
    uncollCheck:SetChecked(uncollectedOnly)
    local uncollLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    uncollLabel:SetPoint("LEFT", uncollCheck, "RIGHT", 2, 0)
    uncollLabel:SetText(L["CHECK_UNCOLLECTED_ONLY"])
    if scheme then ApplyFont(uncollLabel, scheme, "small") end

    -- Collected checkbox
    local collCheck = CreateFrame("CheckButton", nil, filterBar, "UICheckButtonTemplate")
    collCheck:SetSize(20, 20)
    collCheck:SetPoint("LEFT", uncollLabel, "RIGHT", 10, 0)
    collCheck:SetChecked(collectedOnly)
    local collLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    collLabel:SetPoint("LEFT", collCheck, "RIGHT", 2, 0)
    collLabel:SetText(L["STATUS_COLLECTED"])
    if scheme then ApplyFont(collLabel, scheme, "small") end

    -- Mutual exclusion between Uncollected and Collected
    uncollCheck:SetScript("OnClick", function(self)
        uncollectedOnly = self:GetChecked()
        if uncollectedOnly then collectedOnly = false; collCheck:SetChecked(false) end
        StylesTab:RefreshDetail()
    end)
    collCheck:SetScript("OnClick", function(self)
        collectedOnly = self:GetChecked()
        if collectedOnly then uncollectedOnly = false; uncollCheck:SetChecked(false) end
        StylesTab:RefreshDetail()
    end)

    -- Craftable checkbox
    local craftCheck = CreateFrame("CheckButton", nil, filterBar, "UICheckButtonTemplate")
    craftCheck:SetSize(20, 20)
    craftCheck:SetPoint("LEFT", collLabel, "RIGHT", 10, 0)
    craftCheck:SetChecked(craftableOnly)
    craftCheck:SetScript("OnClick", function(self)
        craftableOnly = self:GetChecked()
        StylesTab:RefreshDetail()
    end)
    local craftLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    craftLabel:SetPoint("LEFT", craftCheck, "RIGHT", 2, 0)
    craftLabel:SetText(L["STYLE_FILTER_CRAFTABLE"])
    if scheme then ApplyFont(craftLabel, scheme, "small") end

    -- Hide Unreleased checkbox
    local releasedCheck = CreateFrame("CheckButton", nil, filterBar, "UICheckButtonTemplate")
    releasedCheck:SetSize(20, 20)
    releasedCheck:SetPoint("LEFT", craftLabel, "RIGHT", 10, 0)
    releasedCheck:SetChecked(hideUnreleased)
    releasedCheck:SetScript("OnClick", function(self)
        hideUnreleased = self:GetChecked()
        StylesTab:RefreshDetail()
    end)
    local releasedLabel = filterBar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    releasedLabel:SetPoint("LEFT", releasedCheck, "RIGHT", 2, 0)
    releasedLabel:SetText(L["ACQ_HIDE_UNRELEASED"])
    if scheme then ApplyFont(releasedLabel, scheme, "small") end

    -- Search box
    searchBox = CreateFrame("EditBox", nil, filterBar, "SearchBoxTemplate")
    searchBox:SetSize(140, 20)
    searchBox:SetPoint("RIGHT", -4, 0)
    searchBox:SetAutoFocus(false)
    searchBox:SetScript("OnTextChanged", function(self)
        searchText = self:GetText():lower()
        if searchDebounceTimer then searchDebounceTimer:Cancel() end
        searchDebounceTimer = C_Timer.NewTimer(0.2, function()
            searchDebounceTimer = nil
            StylesTab:RefreshDetail()
        end)
    end)

    -- Room filter buttons
    local roomDefs = {
        { key = nil,          label = "All" },
        { key = "great-hall", label = "Great Hall" },
        { key = "kitchen",    label = "Kitchen" },
        { key = "study",      label = "Study" },
        { key = "bedroom",    label = "Bedroom" },
        { key = "garden",     label = "Garden" },
        { key = "workshop",   label = "Workshop" },
        { key = "trophy",     label = "Trophy" },
    }
    local roomX = 6
    for _, rd in ipairs(roomDefs) do
        local btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
        btn:SetPoint("TOPLEFT", roomX, -96)
        btn.text:SetText(rd.label)

        btn:SetWidth(btn.text:GetStringWidth() + 16)
        btn.roomKey = rd.key
        btn.baseLabel = rd.label
        btn:SetScript("OnClick", function()
            selectedRoom = rd.key
            self:RefreshDetail()
        end)

        roomButtons[#roomButtons + 1] = btn
    end

    -- Item list (left side)
    listFrame = CreateFrame("Frame", nil, detailFrame, "BackdropTemplate")
    listFrame:SetPoint("TOPLEFT", 4, -118)
    listFrame:SetPoint("BOTTOMLEFT", 4, 4)
    listFrame:SetWidth(280)
    HDG_UIHelpers.ApplyBackdrop(listFrame, "panel",
        {0.08, 0.08, 0.08, 0.6}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(listFrame, "Panel")

    itemListScrollBox = CreateFrame("Frame", nil, listFrame, "WowScrollBoxList")
    itemListScrollBox:SetPoint("TOPLEFT", 2, -2)
    itemListScrollBox:SetPoint("BOTTOMRIGHT", -18, 2)

    itemListScrollBar = CreateFrame("EventFrame", nil, listFrame, "MinimalScrollBar")
    itemListScrollBar:SetPoint("TOPLEFT", itemListScrollBox, "TOPRIGHT", 2, 0)
    itemListScrollBar:SetPoint("BOTTOMLEFT", itemListScrollBox, "BOTTOMRIGHT", 2, 0)

    itemListScrollView = CreateScrollBoxListLinearView(0, 0, 0, 0, 0)
    itemListScrollView:SetElementExtentCalculator(function() return ROW_HEIGHT end)
    itemListScrollView:SetElementInitializer("Button", function(frame, elementData)
        StylesTab:_RenderItemListRow(frame, elementData)
    end)

    ScrollUtil.InitScrollBoxListWithScrollBar(itemListScrollBox, itemListScrollBar, itemListScrollView)

    -- Detail panel (right side)
    detailPanel = CreateFrame("Frame", nil, detailFrame, "BackdropTemplate")
    detailPanel:SetPoint("TOPLEFT", listFrame, "TOPRIGHT", 4, 0)
    detailPanel:SetPoint("BOTTOMRIGHT", -4, 4)
    HDG_UIHelpers.ApplyBackdrop(detailPanel, "panel",
        {0.08, 0.08, 0.08, 0.6}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(detailPanel, "Panel")

    -- Detail panel contents
    detailPanel.nameText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    detailPanel.nameText:SetPoint("TOPLEFT", 10, -10)
    if scheme then ApplyFont(detailPanel.nameText, scheme, "header") end

    detailPanel.tierText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailPanel.tierText:SetPoint("TOPLEFT", detailPanel.nameText, "BOTTOMLEFT", 0, -4)
    if scheme then ApplyFont(detailPanel.tierText, scheme, "small") end

    detailPanel.sourceText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailPanel.sourceText:SetPoint("TOPLEFT", detailPanel.tierText, "BOTTOMLEFT", 0, -4)
    if scheme then ApplyFont(detailPanel.sourceText, scheme, "small") end

    detailPanel.reasonText = detailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    detailPanel.reasonText:SetPoint("TOPLEFT", detailPanel.sourceText, "BOTTOMLEFT", 0, -8)
    if scheme then ApplyFont(detailPanel.reasonText, scheme, "small") end
    detailPanel.reasonText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)

    -- 3D Model Preview
    local previewContainer = CreateFrame("Frame", nil, detailPanel, "BackdropTemplate")
    previewContainer:SetPoint("TOPLEFT", detailPanel.reasonText, "BOTTOMLEFT", 0, -10)
    previewContainer:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
    previewContainer:SetPoint("BOTTOM", detailPanel, "BOTTOM", 0, 70)
    HDG_UIHelpers.ApplyBackdrop(previewContainer, "panel",
        {0.05, 0.05, 0.05, 0.8}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(previewContainer, "Panel")
    previewContainer:Hide()
    detailPanel.previewContainer = previewContainer

    local decorPreview = HDG_UIHelpers.CreateDecorPreview(previewContainer, {
        squareAspect = false,
        showOverlays = false,
        showControls = true,
        showCorbels = true,
        sceneInsets = { top = 4, right = 4, bottom = 4, left = 4 },
    })
    decorPreview:SetAllPoints()
    decorPreview.placeholder:Hide() -- container visibility controls this
    detailPanel.decorPreview = decorPreview

    -- Add to Crafting Queue button (for craftable items)
    detailPanel.craftBtn = CreateFrame("Button", nil, detailPanel, "UIPanelButtonTemplate")
    detailPanel.craftBtn:SetSize(180, 24)
    detailPanel.craftBtn:SetPoint("BOTTOMLEFT", 10, 40)
    detailPanel.craftBtn:SetText(L["ACQ_ADD_TO_QUEUE"])
    detailPanel.craftBtn:SetScript("OnClick", function()
        if selectedItemID and StyleEngine and HDG_UI and HDG_UI.AddToCraftingQueue then
            local recipe = StyleEngine:GetRecipe(selectedItemID)
            if recipe then
                local queueItem = {
                    id = recipe.itemID,
                    name = recipe.name,
                    spell = recipe.spellID,
                    reagents = recipe.reagents,
                    expansion = recipe.expansion or "Unknown",
                    profession = recipe.profession,
                }
                HDG_UI.AddToCraftingQueue(queueItem)
                print("|cff00ccff[HDG]|r Added " .. (recipe.name or "recipe") .. " to crafting queue")
            end
        end
    end)
    detailPanel.craftBtn:Hide()

    -- Add to Vendor Shopping List button (for vendor items)
    detailPanel.vendorBtn = CreateFrame("Button", nil, detailPanel, "UIPanelButtonTemplate")
    detailPanel.vendorBtn:SetSize(180, 24)
    detailPanel.vendorBtn:SetPoint("BOTTOMLEFT", 10, 10)
    detailPanel.vendorBtn:SetText(L["STYLE_ADD_TO_SHOPPING_LIST"])
    detailPanel.vendorBtn:SetScript("OnClick", function()
        if selectedItemID and HDG.Store then
            local npcIDs = HDG_VendorByItem and HDG_VendorByItem[selectedItemID]
            if npcIDs and #npcIDs > 0 then
                if HDG.Store:AddToVendorShoppingList(selectedItemID, npcIDs[1]) then
                    local iName = HDG_Data and HDG_Data.GetCachedItemName(selectedItemID) or ("Item " .. selectedItemID)
                    print("|cff00ccff[HDG]|r Added " .. iName .. " to shopping list")
                    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
                end
            end
        end
    end)
    detailPanel.vendorBtn:Hide()

    -- Export button (custom styles only)
    local detailExportBtn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
    detailExportBtn.text:SetText(L["BTN_EXPORT"])
    detailExportBtn:SetWidth(detailExportBtn.text:GetStringWidth() + 20)
    detailExportBtn:SetPoint("TOPRIGHT", -28, -6)
    detailExportBtn:SetScript("OnClick", function()
        if not selectedStyle or not StyleSerializer then return end
        local def = GetStyleDef(selectedStyle)
        if def then
            local encoded = StyleSerializer:Export(def)
            if encoded then StylesTab:ShowExportDialog(encoded) end
        end
    end)
    detailExportBtn:Hide()
    detailFrame.detailExportBtn = detailExportBtn

    -- View toggle button (right of tier buttons)
    local viewToggleBtn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
    viewToggleBtn:SetSize(60, 22)
    viewToggleBtn:SetPoint("TOPLEFT", tierX + 6, -48)
    viewToggleBtn.text:SetText(L["STYLE_VIEW_LIST"])

    viewToggleBtn:SetScript("OnClick", function()
        if viewMode == "gallery" then
            viewMode = "list"
            viewToggleBtn.text:SetText(L["STYLE_VIEW_GRID"])
        else
            viewMode = "gallery"
            viewToggleBtn.text:SetText(L["STYLE_VIEW_LIST"])
        end
        StylesTab:RefreshDetail()
    end)
    detailFrame.viewToggleBtn = viewToggleBtn

    -- Card grid container (hidden by default)
    cardGridFrame = CreateFrame("Frame", nil, detailFrame, "BackdropTemplate")
    cardGridFrame:SetPoint("TOPLEFT", 4, -118)
    cardGridFrame:SetPoint("BOTTOMRIGHT", -4, 4)
    HDG_UIHelpers.ApplyBackdrop(cardGridFrame, "panel",
        {0.08, 0.08, 0.08, 0.6}, {0.2, 0.2, 0.2, 0.4})
    RegisterWidget(cardGridFrame, "Panel")
    cardGridFrame:Hide()

    local ST = CONFIG.STYLES_TAB
    cardGridScrollBox, cardGridScrollBar = HDG.StyleCardRow.Create(cardGridFrame, {
        cellSize         = ST.CELL_SIZE,
        statusIconSize   = ST.STATUS_ICON_SIZE,
        checkmarkTexture = CONFIG.TEXTURES.CHECKMARK,
        sceneID          = ST.DEFAULT_SCENE_ID,
        onEnter          = StyleCell_OnEnter,
        onLeave          = StyleCell_OnLeave,
        onClick          = StyleCell_OnClick,
    })
    cardGridScrollBox:SetPoint("TOPLEFT", 4, -4)
    cardGridScrollBox:SetPoint("BOTTOMRIGHT", -20, 4)

end

-- ============================================================================
-- SHARED DETAIL HELPERS
-- ============================================================================

local function HideDetailFilterButtons()
    for _, btn in pairs(tierButtons) do btn:Hide() end
    for _, btn in ipairs(subcatButtons) do btn:Hide() end
    for _, btn in ipairs(roomButtons) do btn:Hide() end
    for _, btn in ipairs(sourceButtons) do btn:Hide() end
    if detailFrame and detailFrame.viewToggleBtn then detailFrame.viewToggleBtn:Hide() end
end

local function SortByDecorName(filtered)
    local nameOf = {}
    for _, item in ipairs(filtered) do
        local ic = StyleEngine and StyleEngine:GetItemInfo(item.itemID)
        nameOf[item.decorID] = ic and ic.name or ""
    end
    table.sort(filtered, function(a, b) return nameOf[a.decorID] < nameOf[b.decorID] end)
end

-- ============================================================================
-- SHOW / REFRESH
-- ============================================================================

function StylesTab:ShowLanding()
    selectedShoppingListId = nil
    selectedSessionData    = nil
    detailFrame:Hide()
    if StylesTab._editorFrame then StylesTab._editorFrame:Hide() end
    landingFrame:Show()
    statsCacheDirty = true
    self:RefreshLanding()
end

function StylesTab:ShowDetail(styleId)
    selectedStyle       = styleId
    selectedSessionData = nil
    -- Default selected tier/subcategory based on style type
    local def = GetStyleDef(styleId)
    if def and def.tier == "snapshot" then
        selectedTier = nil
        selectedRoom = nil
        selectedSource = nil
    elseif def and def.tier == "collection" then
        selectedTier = nil
        selectedRoom = nil
        selectedSource = nil
    elseif def and def.subcategories then
        selectedTier = def.subcategories[1].key
        selectedSource = nil
    else
        selectedTier = "first"
        selectedRoom = nil
    end
    selectedItemID = nil
    landingFrame:Hide()
    if StylesTab._editorFrame then StylesTab._editorFrame:Hide() end
    detailFrame:Show()
    self:RefreshDetail()
end

function StylesTab:RefreshDetail()
    -- Redirect to shopping list detail if viewing one
    if selectedShoppingListId then
        self:RefreshShoppingListDetail()
        return
    end
    if not selectedStyle or not StyleEngine then return end

    local def = GetStyleDef(selectedStyle)
    if not def then return end

    local scheme = GetScheme()

    -- Update header
    detailHeader:SetText(def.displayName)
    if def.color then
        detailHeader:SetTextColor(def.color.r, def.color.g, def.color.b, 1)
    end
    local descLabel = (def.tier == "curated") and "Curated" or (def.description or "")
    detailDesc:SetText(descLabel)

    -- Show export button for custom/snapshot styles (not collections)
    if detailFrame.detailExportBtn then
        local showExport = (def.tier == "custom" or def.tier == "curated" or def.tier == "snapshot") and StyleSerializer ~= nil
        detailFrame.detailExportBtn:SetShown(showExport)
        if def.tier == "snapshot" then
            detailFrame.detailExportBtn:SetScript("OnClick", function()
                local encoded = StyleSerializer:ExportSnapshot(def)
                if encoded then StylesTab:ShowExportDialog(encoded, def) end
            end)
        else
            detailFrame.detailExportBtn:SetScript("OnClick", function()
                local encoded = StyleSerializer:Export(def)
                if encoded then StylesTab:ShowExportDialog(encoded) end
            end)
        end
    end

    -- Snapshot branch: flat item gallery, no tier/subcategory buttons
    if def.tier == "snapshot" then
        self:RefreshSnapshotDetail(def, scheme)
        return
    end

    -- Restore view toggle (snapshot branch hides it)
    if detailFrame.viewToggleBtn then detailFrame.viewToggleBtn:Show() end

    -- Branch: subcategory vs tier vs collection buttons
    local items = StyleEngine:GetStyleItems(selectedStyle)
    local isSubcat = def.subcategories ~= nil
    local isCollection = (def.tier == "collection")

    local hiddenSet = {}
    for _, bucket in pairs(items) do
        if type(bucket) == "table" then
            for _, it in ipairs(bucket) do
                if IsItemHidden(it.itemID) then hiddenSet[it.itemID] = true end
            end
        end
    end
    local function IsHidden(itemID) return hiddenSet[itemID] end

    if isCollection then
        -- Collections: hide all tier/subcat/room/source buttons, flat item list
        HideDetailFilterButtons()
        if detailFrame.viewToggleBtn then
            detailFrame.viewToggleBtn:ClearAllPoints()
            detailFrame.viewToggleBtn:SetPoint("TOPLEFT", 6, -48)
        end
    elseif isSubcat then
        -- Hide tier buttons
        for _, btn in pairs(tierButtons) do btn:Hide() end
        -- Show/create subcategory buttons
        local scX = 6
        for i, sc in ipairs(def.subcategories) do
            local btn = subcatButtons[i]
            if not btn then
                btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
                subcatButtons[i] = btn
            end
            local count = 0
            if items[sc.key] then
                for _, it in ipairs(items[sc.key]) do
                    if not IsHidden(it.itemID) then count = count + 1 end
                end
            end
            btn.text:SetText(sc.label .. " (" .. count .. ")")
            btn:SetWidth(btn.text:GetStringWidth() + 24)
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", scX, -48)
            btn.subcatKey = sc.key
            btn:SetScript("OnClick", function()
                selectedTier = sc.key
                selectedItemID = nil
                self:RefreshDetail()
            end)
            HDG_UIHelpers.UpdateChipButtonState(btn, sc.key == selectedTier)
            btn:Show()
            scX = scX + btn:GetWidth() + 4
        end
        -- Hide excess subcatButtons
        for j = #def.subcategories + 1, #subcatButtons do
            subcatButtons[j]:Hide()
        end
        -- Reposition view toggle button after subcat buttons
        if detailFrame.viewToggleBtn then
            detailFrame.viewToggleBtn:ClearAllPoints()
            detailFrame.viewToggleBtn:SetPoint("TOPLEFT", scX + 6, -48)
        end
    else
        -- Hide subcategory buttons
        for _, btn in ipairs(subcatButtons) do btn:Hide() end
        -- Show tier buttons (existing behavior)
        local tierCounts = { first = 0, second = 0, neutral = 0, clash = 0 }
        for tk, _ in pairs(tierCounts) do
            if items[tk] then
                for _, it in ipairs(items[tk]) do
                    if not IsHidden(it.itemID) then tierCounts[tk] = tierCounts[tk] + 1 end
                end
            end
        end
        for key, btn in pairs(tierButtons) do
            btn:Show()
            local label = btn.text:GetText():gsub(" %(%d+%)", "")
            btn.text:SetText(label .. " (" .. (tierCounts[key] or 0) .. ")")
            HDG_UIHelpers.UpdateChipButtonState(btn, key == selectedTier)
        end
        -- Restore view toggle position
        if detailFrame.viewToggleBtn then
            detailFrame.viewToggleBtn:ClearAllPoints()
            detailFrame.viewToggleBtn:SetPoint("TOPLEFT", 6 + 4 * 99 + 6, -48)
        end
    end

    -- Get items for current tier/subcategory
    local tierItems
    if isSubcat then
        tierItems = items[selectedTier] or (def.subcategories[1] and items[def.subcategories[1].key]) or {}
    else
        tierItems = items[selectedTier] or items.curated or items.first
    end

    if isCollection then
        -- Collections: no room/source filter row (buttons already hidden above)
    elseif isSubcat then
        -- Source-type buttons (replace rooms for thematic styles)
        for _, btn in ipairs(roomButtons) do btn:Hide() end
        local sourceCounts = {}
        local repCount, endeavorCount, visibleTotal = 0, 0, 0
        for _, item in ipairs(tierItems) do
            if not IsHidden(item.itemID) then
                visibleTotal = visibleTotal + 1
                local st = item.entry[2]
                sourceCounts[st] = (sourceCounts[st] or 0) + 1
                if ItemHasRepRequirement(item.itemID) then repCount = repCount + 1 end
                if ItemCostsEndeavor(item.itemID) then endeavorCount = endeavorCount + 1 end
            end
        end
        sourceCounts["rep"] = repCount
        sourceCounts["endeavor"] = endeavorCount
        local srcX = 6
        for i, sd in ipairs(SOURCE_FILTER_DEFS) do
            local btn = sourceButtons[i]
            if not btn then
                btn = HDG_UIHelpers.CreateChipButton(detailFrame, "", 60, false, nil)
                sourceButtons[i] = btn
            end
            local cnt = sd.key and (sourceCounts[sd.key] or 0) or visibleTotal
            btn.text:SetText(sd.label .. " (" .. cnt .. ")")
            btn:SetWidth(btn.text:GetStringWidth() + 16)
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", srcX, -96)
            btn.sourceKey = sd.key
            btn:SetScript("OnClick", function()
                selectedSource = sd.key
                self:RefreshDetail()
            end)
            HDG_UIHelpers.UpdateChipButtonState(btn, sd.key == selectedSource)
            btn:Show()
            srcX = srcX + btn:GetWidth() + 2
        end
        for j = #SOURCE_FILTER_DEFS + 1, #sourceButtons do
            sourceButtons[j]:Hide()
        end
    else
        -- Room buttons (existing behavior for tier styles)
        for _, btn in ipairs(sourceButtons) do btn:Hide() end
        local roomCounts = {}
        local visibleTotal = 0
        for _, item in ipairs(tierItems) do
            if not IsHidden(item.itemID) then
                visibleTotal = visibleTotal + 1
                local rooms = StyleEngine:GetItemRooms(item.itemID)
                if rooms then
                    for _, r in ipairs(rooms) do
                        roomCounts[r] = (roomCounts[r] or 0) + 1
                    end
                end
            end
        end
        local roomX = 6
        for _, btn in ipairs(roomButtons) do
            local cnt = btn.roomKey and (roomCounts[btn.roomKey] or 0) or visibleTotal
            btn.text:SetText(btn.baseLabel .. " (" .. cnt .. ")")
            btn:SetWidth(btn.text:GetStringWidth() + 16)
            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", roomX, -96)
            roomX = roomX + btn:GetWidth() + 2
            HDG_UIHelpers.UpdateChipButtonState(btn, btn.roomKey == selectedRoom)
            btn:Show()
        end
    end

    -- Build filtered item list for current tier
    local filtered = {}
    for _, item in ipairs(tierItems) do
        local pass = true

        -- Hide unreleased filter
        if pass and IsHidden(item.itemID) then pass = false end

        -- Search filter
        if pass and searchText ~= "" then
            local ic = StyleEngine:GetItemInfo(item.itemID)
            local name = ic and ic.name or ""
            if not (name and name:lower():find(searchText, 1, true)) then
                pass = false
            end
        end

        -- Craftable filter
        if pass and craftableOnly then
            local ic = StyleEngine:GetItemInfo(item.itemID)
            if not (ic and ic.isCraftable) then
                pass = false
            end
        end

        -- Collection filters (mutually exclusive)
        if pass and uncollectedOnly then
            if HDG_Data and HDG_Data.IsItemCollected(item.itemID) then
                pass = false
            end
        elseif pass and collectedOnly then
            if not (HDG_Data and HDG_Data.IsItemCollected(item.itemID)) then
                pass = false
            end
        end

        -- Room filter (tier styles) / Source filter (subcategory styles)
        if pass and isSubcat and selectedSource then
            if selectedSource == "rep" then
                if not ItemHasRepRequirement(item.itemID) then pass = false end
            elseif selectedSource == "endeavor" then
                if not ItemCostsEndeavor(item.itemID) then pass = false end
            else
                local ic = StyleEngine:GetItemInfo(item.itemID)
                if not ic or ic.sourceType ~= selectedSource then pass = false end
            end
        elseif pass and not isSubcat and selectedRoom then
            local ic = StyleEngine:GetItemInfo(item.itemID)
            local rooms = ic and ic.rooms
            if not rooms then
                pass = false
            else
                local found = false
                for _, r in ipairs(rooms) do
                    if r == selectedRoom then found = true; break end
                end
                if not found then pass = false end
            end
        end

        if pass then
            filtered[#filtered + 1] = item
        end
    end

    SortByDecorName(filtered)

    -- Render based on view mode
    if viewMode == "gallery" then
        listFrame:Hide()
        detailPanel:Hide()
        cardGridFrame:Show()
        self:RenderCardGrid(filtered, scheme)
    else
        cardGridFrame:Hide()
        HDG.StyleCardRow.Clear(cardGridScrollBox)
        listFrame:Show()
        detailPanel:Show()
        self:RenderItemList(filtered)
        self:RefreshItemDetail(scheme)
    end
end

-- ============================================================================
-- GALLERY CELL RENDERING
-- ============================================================================

local function UpdateStyleCellHighlights()
    local pool = cardGridScrollBox and cardGridScrollBox._cellPool
    if not pool then return end
    for _, cell in ipairs(pool.active) do
        local wasSelected = cell.isSelected
        cell.isSelected = (selectedItemID == cell.itemID)
        if cell.isSelected ~= wasSelected then
            HDG_UIHelpers.UpdateGalleryCellBackground(cell, cell.isSelected)
        end
    end
end

-- Module-scope gallery cell handlers (avoids closure allocation per cell per render)
-- Assigned to forward-declared locals (declared near top of file).
StyleCell_OnClick = function(self, button)
    if button == "LeftButton" and IsShiftKeyDown() then
        selectedItemID = self.itemID
        UpdateStyleCellHighlights()
    end
end

-- Tier labels (module-level constant; L is loaded before this file runs via TOC order)
local TIER_TOOLTIP_LABELS = {
    ["first-order"] = "|cff00ff00" .. L["STYLE_TIER_SIGNATURE"] .. "|r",
    ["second-order"] = "|cffffff00" .. L["STYLE_TIER_ACCENT"] .. "|r",
    ["neutral"] = "|cffaaaaaa" .. L["STYLE_TIER_VERSATILE"] .. "|r",
    ["clash"] = "|cffff4444" .. L["STYLE_TIER_CLASHING"] .. "|r",
}

StyleCell_OnEnter = function(self)
    self.hoverBg:Show()
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    local ic = StyleEngine and StyleEngine:GetItemInfo(self.itemID)
    local tipName = ic and ic.name or ("Item #" .. self.itemID)
    local tipCollected = HDG_Data.IsItemCollected(self.itemID)
    GameTooltip:AddLine(tipName, 1, 1, 1)
    if tipCollected then
        GameTooltip:AddLine("|cff33cc33" .. L["STATUS_COLLECTED"] .. "|r", 0.2, 0.9, 0.2)
    else
        GameTooltip:AddLine("|cff888888" .. L["STATUS_NOT_COLLECTED"] .. "|r", 0.78, 0.78, 0.78)
    end
    if selectedStyle and StyleEngine then
        local sDef = GetStyleDef(selectedStyle)
        if sDef and sDef.subcategories then
            local reason = StyleEngine:GetTierReason(selectedStyle, self.decorID)
            if reason and reason ~= "" then
                GameTooltip:AddLine(reason, 0.6, 0.8, 0.6, true)
            end
        else
            local tier = StyleEngine:GetCompatibility(selectedStyle, self.decorID)
            if tier and TIER_TOOLTIP_LABELS[tier] then
                GameTooltip:AddLine(string.format(L["TIP_TIER"], TIER_TOOLTIP_LABELS[tier]), 0.8, 0.8, 0.8)
            end
            local reason = StyleEngine:GetTierReason(selectedStyle, self.decorID)
            if reason and reason ~= "" then
                GameTooltip:AddLine(reason, 0.6, 0.6, 0.6, true)
            end
        end
    end
    if ic then
        local sourceLabel = StyleEngine:GetSourceLabel(ic.sourceType)
        GameTooltip:AddLine(string.format(L["TIP_SOURCE"], sourceLabel), 0.7, 0.7, 0.7)
    end
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(L["TIP_SHIFTCLICK_INSPECT"], 0.78, 0.78, 0.78)
    GameTooltip:Show()
end

StyleCell_OnLeave = function(self)
    self.hoverBg:Hide()
    GameTooltip:Hide()
end

function StylesTab:RenderCardGrid(items, scheme)
    local displayItems = {}
    for _, item in ipairs(items) do
        displayItems[#displayItems + 1] = {
            itemID      = item.itemID,
            decorID     = item.decorID,
            entry       = item.entry,
            _isSelected = (selectedItemID == item.itemID),
        }
    end
    HDG.StyleCardRow.SetItems(cardGridScrollBox, displayItems)
end

-- ============================================================================
-- ITEM LIST RENDERING
-- ============================================================================

-- Render dispatcher for itemList ScrollBoxList. Called by the view's
-- element initializer for each visible frame. Mutates `frame` in place.
function StylesTab:_RenderItemListRow(frame, elementData)
    frame:SetWidth(itemListScrollBox:GetWidth())

    if not frame._hdgConstructed then
        ConstructItemListRow(frame)
        frame._hdgConstructed = true
    end

    local item = elementData
    local ic = StyleEngine:GetItemInfo(item.itemID)
    if ic and ic.iconAtlas then
        frame.icon:SetAtlas(ic.iconAtlas)
    elseif ic and ic.iconTexture then
        frame.icon:SetTexture(ic.iconTexture)
    else
        frame.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
    end

    frame.nameText:SetText(ic and ic.name or ("Item #" .. item.itemID))

    local sourceType = ic and ic.sourceType or (item.entry and item.entry[2])
    frame.sourceText:SetText(StyleEngine:GetSourceLabel(sourceType))

    local scheme = GetScheme()
    if scheme then
        ApplyFont(frame.nameText, scheme, "small")
        ApplyFont(frame.sourceText, scheme, "small")
        frame.sourceText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 1)
    end

    frame.selected:SetShown(selectedItemID == item.itemID)
    frame.itemID = item.itemID
    frame.decorID = item.decorID
    frame.entry = item.entry
    frame:SetScript("OnClick", function(self)
        selectedItemID = self.itemID
        -- Update highlights on all rendered frames
        itemListScrollBox:ForEachFrame(function(row)
            if row.selected then row.selected:SetShown(selectedItemID == row.itemID) end
        end)
        StylesTab:RefreshItemDetail(GetScheme())
        StylesTab:LoadPreviewModel(self.decorID)
    end)
end

function StylesTab:RenderItemList(items)
    local provider = CreateDataProvider(items)
    itemListScrollBox:SetDataProvider(provider, ScrollBoxConstants.RetainScrollPosition)
end

-- ============================================================================
-- ITEM DETAIL PANEL
-- ============================================================================

function StylesTab:LoadPreviewModel(decorID)
    if not detailPanel or not detailPanel.decorPreview then return end
    detailPanel._currentDecorID = decorID

    local ok = detailPanel.decorPreview:LoadItemData({ decorID = decorID })
    detailPanel.previewContainer:SetShown(ok)
end

function StylesTab:RefreshItemDetail(scheme)
    if not selectedItemID or not detailPanel then
        detailPanel.nameText:SetText("")
        detailPanel.tierText:SetText("")
        detailPanel.sourceText:SetText("")
        detailPanel.reasonText:SetText("")
        detailPanel.craftBtn:Hide()
        detailPanel.vendorBtn:Hide()
        if detailPanel.previewContainer then detailPanel.previewContainer:Hide() end
        detailPanel._currentDecorID = nil
        return
    end

    -- Find the item data
    local entry = HDG_AllDecorDB[selectedItemID]
    if not entry then return end
    local decorID = entry[1]

    -- Name
    local ic = StyleEngine:GetItemInfo(selectedItemID)
    detailPanel.nameText:SetText(ic and ic.name or ("Item #" .. selectedItemID))

    -- Tier / Subcategory label
    local sDef = GetStyleDef(selectedStyle)
    if sDef and sDef.subcategories then
        local subcatLabel = StyleEngine:GetSubcategoryLabel(selectedStyle, selectedTier)
        detailPanel.tierText:SetText("|cff88cc88" .. subcatLabel .. "|r")
    else
        local tier = StyleEngine:GetCompatibility(selectedStyle, decorID)
        local tierLabels = {
            ["first-order"] = "|cff00ff001ST ORDER|r",
            ["second-order"] = "|cffffff002ND ORDER|r",
            ["neutral"] = "|cffaaaaaaNEUTRAL|r",
            ["clash"] = "|cffff4444CLASH|r",
            ["unclassified"] = "|cff888888UNCLASSIFIED|r",
        }
        detailPanel.tierText:SetText("Compatibility: " .. (tierLabels[tier] or tier))
    end

    -- Source
    local sourceType = entry[2]
    local sourceName = entry[3] or ""
    local sourceDetail = entry[4] or ""
    local sourceLabel = StyleEngine:GetSourceLabel(sourceType)
    if sourceType ~= 5 and HDG_VendorResolver and HDG_VendorResolver.GetVendorsForItem then
        local vendors = HDG_VendorResolver.GetVendorsForItem(selectedItemID)
        if vendors and #vendors > 0 then
            sourceLabel = sourceLabel .. " + Vendor"
        end
    end
    local sourceStr = string.format(L["TIP_SOURCE"], sourceLabel)
    if #sourceName > 0 then sourceStr = sourceStr .. " - " .. sourceName end
    if #sourceDetail > 0 then sourceStr = sourceStr .. " (" .. sourceDetail .. ")" end
    detailPanel.sourceText:SetText(sourceStr)
    if scheme then
        ApplyFont(detailPanel.sourceText, scheme, "small")
        local s = GetScheme()
        if s then detailPanel.sourceText:SetTextColor(s.text.r, s.text.g, s.text.b, 1) end
    end

    -- Why this tier
    local reason = StyleEngine:GetTierReason(selectedStyle, decorID)
    detailPanel.reasonText:SetText(reason)

    -- Show action buttons based on item source
    local recipe = StyleEngine:GetRecipe(selectedItemID)
    local npcIDs = HDG_VendorByItem and HDG_VendorByItem[selectedItemID]
    if recipe then
        detailPanel.craftBtn:Show()
    else
        detailPanel.craftBtn:Hide()
    end
    if npcIDs and #npcIDs > 0 then
        detailPanel.vendorBtn:Show()
    else
        detailPanel.vendorBtn:Hide()
    end

    -- 3D preview
    self:LoadPreviewModel(decorID)
end


-- Delete confirmation popup
StaticPopupDialogs["HDG_DELETE_CUSTOM_STYLE"] = {
    text = L["STYLE_CONFIRM_DELETE"],
    button1 = L["STYLE_DELETE"],
    button2 = L["STYLE_CANCEL"],
    OnAccept = function(self, data)
        if data and data.key and HDG.Store then
            HDG.Store:DeleteCustomStyle(data.key)
            if StyleEngine then
                StyleEngine:RemoveCustomStyle(data.key)
            end
            if selectedStyle == data.key then
                selectedStyle = nil
            end
            StylesTab:ShowLanding()
        end
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

-- ============================================================================
-- SNAPSHOT DETAIL VIEW
-- ============================================================================

-- Shared gallery renderer for snapshot + shopping list detail views.
-- badgeField: item field name for the badge value (e.g. "placed", "qty"), or nil
-- badgePrefix: string prefix for badge text (e.g. "x"), or ""
-- badgeColor: {r,g,b} for infoText, defaults to {0.4, 0.7, 1.0}
-- sortFn: optional sort function(filtered) -- if nil, SortByDecorName is used
-- extraTooltipFn: function(GameTooltip, item) called after the item name line
local function RefreshFlatGalleryDetail(filtered, badgeField, badgePrefix, badgeColor, sortFn, extraTooltipFn)
    if sortFn then
        sortFn(filtered)
    else
        SortByDecorName(filtered)
    end

    listFrame:Hide()
    detailPanel:Hide()
    cardGridFrame:Show()

    local bc = badgeColor or { 0.4, 0.7, 1.0 }
    local displayItems = {}
    for _, item in ipairs(filtered) do
        local cd = {
            itemID  = item.itemID,
            decorID = item.decorID,
            entry   = item.entry,
        }
        -- Badge
        if badgeField then
            local val = item[badgeField]
            if val and val > 0 then
                cd._badgeText   = (badgePrefix or "") .. val
                cd._badgeColorR = bc[1]
                cd._badgeColorG = bc[2]
                cd._badgeColorB = bc[3]
            end
        end
        -- Per-item OnEnter closure (captures item + extraTooltipFn)
        local capturedItem = item
        cd._onEnter = function(self2)
            self2.hoverBg:Show()
            GameTooltip:SetOwner(self2, "ANCHOR_RIGHT")
            local ic = StyleEngine and StyleEngine:GetItemInfo(self2.itemID)
            local tipName = ic and ic.name or ("Item #" .. self2.itemID)
            GameTooltip:AddLine(tipName, 1, 1, 1)
            if extraTooltipFn then extraTooltipFn(GameTooltip, capturedItem) end
            GameTooltip:Show()
        end
        cd._onLeave = StyleCell_OnLeave
        displayItems[#displayItems + 1] = cd
    end
    HDG.StyleCardRow.SetItems(cardGridScrollBox, displayItems)
end

function StylesTab:RefreshSnapshotDetail(def, scheme)
    HideDetailFilterButtons()

    -- Update description with snapshot stats
    local totalPlaced, totalStored, uniqueItems = 0, 0, 0
    if def.snapshot and def.snapshot.items then
        for _, counts in pairs(def.snapshot.items) do
            uniqueItems = uniqueItems + 1
            totalPlaced = totalPlaced + (counts.placed or 0)
            totalStored = totalStored + (counts.stored or 0)
        end
    end
    detailDesc:SetText(totalPlaced .. " placed across all houses, " .. totalStored .. " in storage (" .. uniqueItems .. " unique)")

    local items = StyleEngine:GetStyleItems(selectedStyle)
    local snapshotItems = items and items.items or {}

    local filtered = {}
    for _, item in ipairs(snapshotItems) do
        if searchText == "" or (function()
            local ic = StyleEngine:GetItemInfo(item.itemID)
            local name = ic and ic.name or ""
            return name:lower():find(searchText, 1, true)
        end)() then
            filtered[#filtered + 1] = item
        end
    end

    local function snapshotSort(list)
        local nameOf = {}
        for _, item in ipairs(list) do
            local ic = StyleEngine:GetItemInfo(item.itemID)
            nameOf[item.decorID] = ic and ic.name or ""
        end
        table.sort(list, function(a, b)
            if (a.placed or 0) ~= (b.placed or 0) then return (a.placed or 0) > (b.placed or 0) end
            return nameOf[a.decorID] < nameOf[b.decorID]
        end)
    end

    RefreshFlatGalleryDetail(filtered, "placed", "x", {0.4, 0.7, 1.0}, snapshotSort, function(tt, item)
        if item.placed and item.placed > 0 then
            tt:AddLine(string.format(L["TIP_PLACED"], item.placed), 0.4, 0.7, 1.0)
        end
        if item.stored and item.stored > 0 then
            tt:AddLine(string.format(L["TIP_IN_STORAGE"], item.stored), 0.6, 0.6, 0.6)
        end
    end)
end

-- ============================================================================
-- SHOPPING LIST DETAIL VIEW
-- ============================================================================

function StylesTab:ShowShoppingListDetail(listId)
    selectedShoppingListId = listId
    selectedStyle          = nil
    selectedSessionData    = nil
    selectedItemID         = nil
    landingFrame:Hide()
    if StylesTab._editorFrame then StylesTab._editorFrame:Hide() end
    detailFrame:Show()
    self:RefreshShoppingListDetail()
end

function StylesTab:RefreshShoppingListDetail()
    if not selectedShoppingListId or not HDG.Store then return end

    local allLists = HDG.Store.state and HDG.Store.state.vendorShoppingLists
    local listData = allLists and allLists[selectedShoppingListId]
    if not listData then return end

    local scheme = GetScheme()

    -- Update header
    detailHeader:SetText(listData.name or L["HEADER_SHOPPING_LIST"])
    if scheme then
        detailHeader:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, 1)
    end
    local itemCount = listData.items and #listData.items or 0
    detailDesc:SetText(string.format(L["STYLE_ITEMS_IN_LIST"], itemCount))

    -- Hide export button
    if detailFrame.detailExportBtn then detailFrame.detailExportBtn:Hide() end

    HideDetailFilterButtons()

    -- Build items from shopping list entries
    local items = {}
    for _, entry in ipairs(listData.items or {}) do
        local itemID = entry.itemID or entry.id
        local qty = entry.qty or entry.quantity or 1
        local ade = HDG_AllDecorDB and HDG_AllDecorDB[itemID]
        local decorID = ade and ade[1]
        if itemID and decorID then
            items[#items + 1] = {
                itemID = itemID,
                decorID = decorID,
                entry = ade,
                qty = qty,
            }
        end
    end

    -- Filter by search and collection state
    local filtered = {}
    for _, item in ipairs(items) do
        local pass = true
        if searchText ~= "" then
            local ic = StyleEngine:GetItemInfo(item.itemID)
            local name = ic and ic.name or ""
            if not (name and name:lower():find(searchText, 1, true)) then pass = false end
        end
        if pass and uncollectedOnly then
            if HDG_Data and HDG_Data.IsItemCollected(item.itemID) then pass = false end
        elseif pass and collectedOnly then
            if not (HDG_Data and HDG_Data.IsItemCollected(item.itemID)) then pass = false end
        end
        if pass then filtered[#filtered + 1] = item end
    end

    RefreshFlatGalleryDetail(filtered, "qty", "", {1, 1, 1}, nil, function(tt, item)
        if item.qty and item.qty > 1 then
            tt:AddLine(string.format(L["TIP_QTY"], item.qty), 0.7, 0.7, 0.7)
        end
        local isCollected = HDG_Data.IsItemCollected(item.itemID)
        if isCollected then
            tt:AddLine(L["STATUS_COLLECTED"], 0.4, 0.8, 0.4)
        else
            tt:AddLine(L["STATUS_NOT_COLLECTED"], 0.8, 0.4, 0.4)
        end
    end)
end

-- ============================================================================
-- SNAPSHOT PLACEMENT COST
-- ============================================================================

function StylesTab:ComputeSnapshotCost(def)
    if not def or not def.snapshot or not def.snapshot.items then return end
    if not (HDG_Data and HDG_Data.GetCatalog) then return end
    if not HDG_AllDecorDB then return end
    local cost = 0
    for itemID, counts in pairs(def.snapshot.items) do
        local entry = HDG_AllDecorDB[itemID]
        if entry then
            local info = HDG_Data.GetCatalog(entry[1])
            if info and info.placementCost then
                cost = cost + info.placementCost * (counts.placed or 0)
            end
        end
    end
    def.snapshot.totalPlacementCost = cost
end

-- ============================================================================
-- SNAPSHOT CAPTURE
-- ============================================================================

function StylesTab:TakeSnapshot()
    -- Collect all itemIDs to scan
    local allItemIDs = {}
    for itemID in pairs(HDG_AllDecorDB) do
        allItemIDs[#allItemIDs + 1] = itemID
    end
    table.sort(allItemIDs)

    local snapshotItems = {}
    local totalCost = 0
    local idx = 0
    local total = #allItemIDs
    local BATCH_SIZE = 200
    local dbgOk, dbgNil, dbgHit = 0, 0, 0

    -- Update button text to show scanning
    if landingFrame and landingFrame.snapshotBtn then
        landingFrame.snapshotBtn.text:SetText(L["GOBLIN_SCANNING"])
        landingFrame.snapshotBtn:Disable()
    end

    local function ProcessBatch()
        local batchEnd = math.min(idx + BATCH_SIZE, total)
        for i = idx + 1, batchEnd do
            local itemID = allItemIDs[i]
            local entry = HDG_AllDecorDB[itemID]
            if entry then
                local decorID = entry[1]
                local catInfo = HDG_Data and HDG_Data.GetCatalog and HDG_Data.GetCatalog(decorID)
                if catInfo then
                    dbgOk = dbgOk + 1
                    local placed = catInfo.numPlaced or 0
                    if placed > 0 then
                        dbgHit = dbgHit + 1
                        local stored = catInfo.quantity or 0
                        snapshotItems[itemID] = { placed = placed, stored = stored }
                        if catInfo.placementCost then totalCost = totalCost + catInfo.placementCost * placed end
                    end
                else
                    dbgNil = dbgNil + 1
                end
            end
        end
        idx = batchEnd

        if idx < total then
            C_Timer.After(0, ProcessBatch)
        else
            print("|cFF14b8a6[HDG Snapshot]|r Catalog scan: " .. total .. " items, " .. dbgOk .. " ok, " .. dbgNil .. " nil, " .. dbgHit .. " placed (account-wide)")
            StylesTab:FinishSnapshot(snapshotItems, totalCost)
        end
    end

    ProcessBatch()
end

function StylesTab:FinishSnapshot(snapshotItems, totalCost)
    -- Restore button
    if landingFrame and landingFrame.snapshotBtn then
        landingFrame.snapshotBtn.text:SetText(L["STYLE_SAVE_PLACED_DECOR"])
        landingFrame.snapshotBtn:Enable()
    end

    -- Check if we found anything
    local itemCount = 0
    local totalPlaced = 0
    for _, counts in pairs(snapshotItems) do
        itemCount = itemCount + 1
        totalPlaced = totalPlaced + (counts.placed or 0)
    end

    if itemCount == 0 then
        print("|cFFFF4444[HDG]|r No placed or stored decor found")
        return
    end

    -- Show name dialog
    StaticPopupDialogs["HDG_SNAPSHOT_NAME"] = StaticPopupDialogs["HDG_SNAPSHOT_NAME"] or {
        text = "Name your snapshot (all placed decor, account-wide):",
        button1 = "Save",
        button2 = "Cancel",
        hasEditBox = true,
        editBoxWidth = 250,
        OnShow = function(self)
            self.EditBox:SetText("All Placed Decor")
            self.EditBox:HighlightText()
            self.EditBox:SetFocus()
        end,
        OnAccept = function(self)
            local name = self.EditBox:GetText()
            if not name or name == "" then name = "All Placed Decor" end
            StylesTab:SaveSnapshot(name, self.data.items, self.data.totalCost)
        end,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        preferredIndex = 3,
    }

    StaticPopup_Show("HDG_SNAPSHOT_NAME", nil, nil, { items = snapshotItems, totalCost = totalCost or 0 })
end

function StylesTab:SaveSnapshot(name, snapshotItems, totalCost)
    local totalPlaced = 0
    for _, counts in pairs(snapshotItems) do
        totalPlaced = totalPlaced + (counts.placed or 0)
    end

    local def = {
        displayName = name,
        iconAtlas = "decor-controls-decoratemode-active",
        tier = "snapshot",
        color = { r = 0.4, g = 0.7, b = 1.0 },
        description = totalPlaced .. " placed -- " .. date("%b %d, %Y"),
        snapshot = {
            capturedAt = time(),
            items = snapshotItems,
            totalPlacementCost = totalCost or 0,
        },
    }

    local key = "snapshot-" .. time() .. "-" .. math.random(1000, 9999)
    HDG.Store:SaveCustomStyle(key, def)
    if StyleEngine then
        StyleEngine:InvalidateCustomStyle(key)
    end

    print("|cFF00FF00[HDG]|r Snapshot saved: " .. name .. " (" .. totalPlaced .. " items placed)")

    if panel and panel:IsShown() then
        self:ShowLanding()
    end
end

-- ============================================================================
-- EXPORT DIALOG
-- ============================================================================

local exportCtrl

function StylesTab:ShowExportDialog(encodedStr, snapshotDef)
    if not exportCtrl then
        exportCtrl = HDG_UIHelpers.CreateExportDialog({ width = 500, height = 220, bottomPad = 65 })
        -- Decor ID checkbox (shown only for snapshot exports)
        local decorCB = CreateFrame("CheckButton", nil, exportCtrl.frame, "UICheckButtonTemplate")
        decorCB:SetSize(22, 22)
        decorCB:SetPoint("BOTTOMLEFT", 12, 32)
        local decorLabel = exportCtrl.frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        decorLabel:SetPoint("LEFT", decorCB, "RIGHT", 2, 0)
        decorLabel:SetText(L["EXPORT_USE_DECOR_IDS"])
        local scheme = HDG_UIHelpers.GetScheme()
        if scheme then HDG.Theme.ApplyFont(decorLabel, scheme, "small") end
        decorCB:Hide()
        exportCtrl.decorCB = decorCB
    end

    exportCtrl:Show(encodedStr, "|cFF14b8a6" .. L["STYLE_EXPORT_TITLE"] .. "|r")

    -- Show decorID checkbox only for snapshot exports
    if snapshotDef and snapshotDef.snapshot and StyleSerializer then
        exportCtrl.decorCB:SetChecked(false)
        exportCtrl.decorCB:Show()
        exportCtrl.decorCB:SetScript("OnClick", function(self)
            local useDecor = self:GetChecked()
            local reEncoded = StyleSerializer:ExportSnapshot(snapshotDef, useDecor)
            if reEncoded then
                exportCtrl.editBox:SetText(reEncoded)
                exportCtrl.editBox:HighlightText()
                exportCtrl.editBox:SetFocus()
            end
        end)
    else
        exportCtrl.decorCB:Hide()
    end
end

-- ============================================================================
-- IMPORT DIALOG
-- ============================================================================

StaticPopupDialogs["HDG_IMPORT_STYLE"] = {
    text = L["STYLE_IMPORT_PROMPT"],
    button1 = L["STYLE_IMPORT"],
    button2 = L["STYLE_CANCEL"],
    hasEditBox = true,
    editBoxWidth = 350,
    OnShow = function(self)
        self.EditBox:SetMaxLetters(0) -- unlimited (default 255 truncates long HDGVL strings)
        self.EditBox:SetText("")
        self.EditBox:SetFocus()
    end,
    OnAccept = function(self)
        local text = self.EditBox:GetText()
        if not text or text == "" then return end
        if not StyleSerializer then return end

        local styleDef, err = StyleSerializer:Import(text)
        if not styleDef then
            print("|cFFFF4444[HDG]|r " .. string.format(L["STYLE_MSG_IMPORT_FAILED"], err or "unknown error"))
            return
        end

        -- Compute placement cost for snapshot imports
        if styleDef.tier == "snapshot" and styleDef.snapshot and not styleDef.snapshot.totalPlacementCost then
            StylesTab:ComputeSnapshotCost(styleDef)
        end

        -- Save (use snapshot key prefix for snapshot imports)
        local key
        if styleDef.tier == "snapshot" then
            key = "snapshot-" .. time() .. "-" .. math.random(1000, 9999)
        end
        key = HDG.Store:SaveCustomStyle(key, styleDef)
        if StyleEngine then
            StyleEngine:InvalidateCustomStyle(key)
        end

        print("|cFF00FF00[HDG]|r " .. string.format(L["STYLE_MSG_IMPORTED"], styleDef.displayName))
        if StylesTab and panel and panel:IsShown() then
            StylesTab:ShowLanding()
        end
    end,
    timeout = 0,
    whileDead = true,
    hideOnEscape = true,
    preferredIndex = 3,
}

function StylesTab:ShowImportDialog()
    StaticPopup_Show("HDG_IMPORT_STYLE")
end

-- ============================================================================
-- RECENT SESSION DETAIL VIEW
-- ============================================================================

function StylesTab:ShowSessionDetail(houseGUID, sessionID, displayName)
    if not (houseGUID and sessionID) then return end
    selectedSessionData    = { houseGUID = houseGUID, sessionID = sessionID, displayName = displayName }
    selectedShoppingListId = nil
    selectedStyle          = nil
    selectedItemID         = nil
    landingFrame:Hide()
    if StylesTab._editorFrame then StylesTab._editorFrame:Hide() end
    detailFrame:Show()
    self:RefreshSessionDetail()
end

function StylesTab:RefreshSessionDetail()
    if not selectedSessionData then return end
    local houseGUID   = selectedSessionData.houseGUID
    local sessionID   = selectedSessionData.sessionID
    local displayName = selectedSessionData.displayName or "Edit Session"

    if not (HDG.RecentActivity and HDG.RecentActivity.GetEventGroups) then return end

    local scheme = GetScheme()
    local groups = HDG.RecentActivity:GetEventGroups(houseGUID, sessionID)

    -- Header
    detailHeader:SetText(displayName)
    if scheme then
        detailHeader:SetTextColor(0.6, 0.7, 0.9, 1)
    end
    detailDesc:SetText(#groups .. " decor types")

    -- Hide export button (read-only view)
    if detailFrame.detailExportBtn then detailFrame.detailExportBtn:Hide() end
    if detailFrame.viewToggleBtn   then detailFrame.viewToggleBtn:Hide()   end

    HideDetailFilterButtons()
    listFrame:Hide()
    detailPanel:Hide()
    cardGridFrame:Show()

    local displayItems = {}
    for _, g in ipairs(groups) do
        displayItems[#displayItems + 1] = {
            decorID             = g.decorID,
            _recentEventType    = g.eventType,
            _recentLiveCount    = g.liveCount,
            _recentDeadCount    = g.deadCount,
            _recentEventCount   = g.eventCount,
            _recentName         = g.name,
            _recentIcon         = g.icon,
            _readOnly           = true,
        }
    end
    HDG.StyleCardRow.SetItems(cardGridScrollBox, displayItems)
end

