-- HousingDecorGuide/UI/Tabs/Styles/HDG_ST_Detail.lua
-- Detail-screen helpers for the Styles tab. First-chunk extraction: pure
-- helpers + gallery-cell event handlers + selection-highlight refresh.
--
-- Widget creation (CreateDetail), the main RefreshDetail branching, and
-- the snapshot/session/shopping-list render variants stay in
-- HDG_StylesTab.lua for now -- they're tightly coupled to module-local
-- frame anchors. Detail module's surface will grow in subsequent extractions.
--
-- API:
--   Detail:Init(refs)             -- capture widget refs from CreateDetail
--   Detail:HideFilterButtons()    -- hide all chip rows (tier/subcat/room/source)
--   Detail:UpdateCellHighlights() -- repaint cell selection state on the grid
--   Detail.SortByDecorName(items) -- mutates the array, sorts by decor name
--   Detail.OnCellClick / OnCellEnter / OnCellLeave -- gallery cell handlers
--
-- Loaded after HDG_ST_State.lua so privateDB.HDG_ST.State is available.

local _, privateDB = ...

local HDG = _G.HousingDecorGuide
local L = HDG_L or {}

privateDB.HDG_ST = privateDB.HDG_ST or {}
local State = privateDB.HDG_ST.State

local M = {}
privateDB.HDG_ST.Detail = M

-- =============================================================================
-- INIT: capture widget refs from the orchestrator's CreateDetail
-- =============================================================================

-- refs shape: {
--   tierButtons, subcatButtons, roomButtons, sourceButtons -- chip-row arrays
--   listFrame, detailPanel, cardGridFrame                  -- detail-frame widgets
--   cardGridScrollBox                                      -- gallery scrollbox
--   detailFrame, detailHeader, detailDesc                  -- header / subtitle text
-- }
function M:Init(refs)
    self._refs = refs or {}
end

local function GetScheme()        return HDG.Theme.GetCurrentScheme() end
local function ApplyFont(fs, s, ft) HDG.Theme.ApplyFont(fs, s, ft) end

-- =============================================================================
-- CONSTANTS (for tier-style detail rendering)
-- =============================================================================

local ROW_HEIGHT = 22

-- Source-type filter chips (replaces room chips for thematic styles).
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" },
}
M.SOURCE_FILTER_DEFS = SOURCE_FILTER_DEFS
M.ROW_HEIGHT         = ROW_HEIGHT

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

-- Tier tooltip labels: shown on cell hover for tier-mode styles. L is loaded
-- before this file via TOC order so the lookups resolve at module load.
M.TIER_TOOLTIP_LABELS = {
    ["first-order"]  = "|cff00ff00" .. (L["STYLE_TIER_SIGNATURE"] or "")  .. "|r",
    ["second-order"] = "|cffffff00" .. (L["STYLE_TIER_ACCENT"]    or "")  .. "|r",
    ["neutral"]      = "|cffaaaaaa" .. (L["STYLE_TIER_VERSATILE"] or "")  .. "|r",
    ["clash"]        = "|cffff4444" .. (L["STYLE_TIER_CLASHING"]  or "")  .. "|r",
}

-- =============================================================================
-- HELPERS
-- =============================================================================

local function GetStyleEngine() return privateDB and privateDB.HDG_StyleEngine end

local function GetStyleDef(styleId)
    local SE = GetStyleEngine()
    if SE and SE.GetAllStyleDefs then
        local defs = SE:GetAllStyleDefs()
        return defs and defs[styleId]
    end
    return nil
end

-- Hide every chip row that may still be visible from a previous Detail
-- render. Called by RefreshDetail when entering a flat-render style
-- (curated/collection/snapshot/shopping list).
function M:HideFilterButtons()
    local r = self._refs
    if not r then return end
    if r.tierButtons    then for _, btn in pairs(r.tierButtons)    do btn:Hide() end end
    if r.subcatButtons  then for _, btn in ipairs(r.subcatButtons) do btn:Hide() end end
    if r.roomButtons    then for _, btn in ipairs(r.roomButtons)   do btn:Hide() end end
    if r.sourceButtons  then for _, btn in ipairs(r.sourceButtons) do btn:Hide() end end
    -- viewToggleBtn lives in the top-right utility cluster; not a chip row.
end

-- Hide-unreleased filter — items get hidden from any visible/count surface
-- when they're flagged isUnreleased AND the user has Hide Unreleased on.
local function IsItemHidden(itemID)
    if not State.hideUnreleased then return false end
    local SE = GetStyleEngine()
    local ic = SE and SE:GetItemInfo(itemID)
    return not ic or ic.isUnreleased
end
M.IsItemHidden = IsItemHidden

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

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

-- One-time row construction for the item-list scrollbox. Mutates `row` in
-- place so the WowScrollBoxList recycles the populated frame across renders.
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

-- In-place alphabetical sort by decor name (StyleEngine:GetItemInfo).
function M.SortByDecorName(filtered)
    local SE = GetStyleEngine()
    local nameOf = {}
    for _, item in ipairs(filtered) do
        local ic = SE and SE: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

-- Walk the visible rowFrames in the gallery and re-apply selection-state
-- background. Called when selectedItemID changes (cell click).
function M:UpdateCellHighlights()
    local scrollBox = self._refs and self._refs.cardGridScrollBox
    if not scrollBox then return end
    scrollBox:ForEachFrame(function(rowFrame)
        if not rowFrame._cards then return end
        for _, cell in ipairs(rowFrame._cards) do
            local wasSelected = cell.isSelected
            cell.isSelected = (State.selectedItemID == cell.itemID)
            if cell.isSelected ~= wasSelected then
                HDG_UIHelpers.UpdateGalleryCellBackground(cell, cell.isSelected)
            end
        end
    end)
end

-- =============================================================================
-- GALLERY CELL EVENT HANDLERS
-- =============================================================================
-- Module-scope (no per-cell closures). Wired into HDG.StyleCardRow via
-- Detail.OnCellClick/OnCellEnter/OnCellLeave. State drives the selection
-- model; UpdateCellHighlights repaints when click changes selection.

function M.OnCellClick(self, button)
    if button == "LeftButton" and IsShiftKeyDown() then
        State.selectedItemID = self.itemID
        M:UpdateCellHighlights()
    end
end

function M.OnCellEnter(self)
    self.hoverBg:Show()
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")

    local SE = GetStyleEngine()
    local ic = SE and SE:GetItemInfo(self.itemID)
    local tipName = ic and ic.name or ("Item #" .. self.itemID)
    local tipCollected = HDG_Data and 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 State.selectedStyle and SE then
        local sDef = GetStyleDef(State.selectedStyle)
        if sDef and sDef.subcategories then
            local reason = SE:GetTierReason(State.selectedStyle, self.decorID)
            if reason and reason ~= "" then
                GameTooltip:AddLine(reason, 0.6, 0.8, 0.6, true)
            end
        else
            local tier = SE:GetCompatibility(State.selectedStyle, self.decorID)
            if tier and M.TIER_TOOLTIP_LABELS[tier] then
                GameTooltip:AddLine(string.format(L["TIP_TIER"], M.TIER_TOOLTIP_LABELS[tier]), 0.8, 0.8, 0.8)
            end
            local reason = SE:GetTierReason(State.selectedStyle, self.decorID)
            if reason and reason ~= "" then
                GameTooltip:AddLine(reason, 0.6, 0.6, 0.6, true)
            end
        end
    end

    if ic and SE then
        local sourceLabel = SE: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

function M.OnCellLeave(self)
    self.hoverBg:Hide()
    GameTooltip:Hide()
end

-- =============================================================================
-- FLAT GALLERY RENDER (snapshot + shopping-list detail share this)
-- =============================================================================
-- Renders a flat list of decor cards into the cardGridFrame. Hides the
-- list/detail panes (those are tier-style features). Caller supplies:
--   filtered       -- array of { itemID, decorID, entry, [badgeField], ... }
--   badgeField     -- field name on each item to read for the corner badge
--                     (e.g. "placed", "qty"); nil to skip badge
--   badgePrefix    -- string prefix to prepend to the badge value (e.g. "x")
--   badgeColor     -- {r, g, b} for badge text; defaults to cool blue
--   sortFn         -- optional sort(filtered); falls back to SortByDecorName
--   extraTooltipFn -- function(GameTooltip, item) appended after item name

function M:RenderFlatGallery(filtered, badgeField, badgePrefix, badgeColor, sortFn, extraTooltipFn)
    if sortFn then
        sortFn(filtered)
    else
        M.SortByDecorName(filtered)
    end

    local r = self._refs
    if r.listFrame    then r.listFrame:Hide()    end
    if r.detailPanel  then r.detailPanel:Hide()  end
    if r.cardGridFrame then r.cardGridFrame:Show() end

    local SE = GetStyleEngine()
    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,
        }
        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(cell)
            cell.hoverBg:Show()
            GameTooltip:SetOwner(cell, "ANCHOR_RIGHT")
            local ic = SE and SE:GetItemInfo(cell.itemID)
            local tipName = ic and ic.name or ("Item #" .. cell.itemID)
            GameTooltip:AddLine(tipName, 1, 1, 1)
            if extraTooltipFn then extraTooltipFn(GameTooltip, capturedItem) end
            GameTooltip:Show()
        end
        cd._onLeave = M.OnCellLeave
        displayItems[#displayItems + 1] = cd
    end
    HDG.StyleCardRow.SetItems(r.cardGridScrollBox, displayItems)
end

-- =============================================================================
-- SNAPSHOT DETAIL RENDER
-- =============================================================================
-- Snapshot styles capture all decor placed across all houses at a moment in
-- time. Rendered as a flat gallery with per-card "x{N} placed" badges.

function M:RenderSnapshot(def)
    self:HideFilterButtons()

    local r = self._refs

    -- Subtitle: snapshot stats summary.
    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
    if r.detailDesc then
        r.detailDesc:SetText(totalPlaced .. " placed across all houses, " ..
                             totalStored .. " in storage (" .. uniqueItems .. " unique)")
    end

    local SE = GetStyleEngine()
    local items = SE and SE:GetStyleItems(State.selectedStyle) or {}
    local snapshotItems = items and items.items or {}

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

    -- Snapshot sort: most-placed first, then alphabetical by decor name.
    local function snapshotSort(list)
        local nameOf = {}
        for _, item in ipairs(list) do
            local ic = SE and SE: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

    self:RenderFlatGallery(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 RENDER
-- =============================================================================
-- Shopping lists rented the detail frame for their own item-list view. Items
-- come from HDG.Store's vendorShoppingLists; rendered as a flat gallery with
-- per-card qty badges.

function M:RenderShoppingList()
    if not State.selectedShoppingListId or not HDG.Store then return end

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

    local r = self._refs
    local scheme = GetScheme()

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

    if r.detailFrame and r.detailFrame.detailExportBtn then
        r.detailFrame.detailExportBtn:Hide()
    end

    self:HideFilterButtons()

    local SE = GetStyleEngine()
    local items = {}
    for _, entry in ipairs(listData.items or {}) do
        local itemID = entry.itemID or entry.id
        local qty = entry.qty 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 searchText = State.searchText or ""
    local filtered = {}
    for _, item in ipairs(items) do
        local pass = true
        if searchText ~= "" then
            local ic = SE and SE:GetItemInfo(item.itemID)
            local name = ic and ic.name or ""
            if not name:lower():find(searchText, 1, true) then pass = false end
        end
        if pass and State:IsUncollectedOnly() then
            if HDG_Data and HDG_Data.IsItemCollected(item.itemID) then pass = false end
        elseif pass and State:IsCollectedOnly() 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

    self:RenderFlatGallery(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 and 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

-- =============================================================================
-- TIER-STYLE GRID + LIST RENDER
-- =============================================================================

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

-- Item-list row initializer (called by the WowScrollBoxList view's
-- SetElementInitializer). Mutates `frame` in place; the per-frame
-- _hdgConstructed guard keeps construction one-shot per row pool entry.
function M:_RenderItemListRow(frame, elementData)
    local r = self._refs
    local SE = GetStyleEngine()
    if r.itemListScrollBox then
        frame:SetWidth(r.itemListScrollBox:GetWidth())
    end

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

    local item = elementData
    local ic = SE and SE: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(SE and SE:GetSourceLabel(sourceType) or "")

    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(State.selectedItemID == item.itemID)
    frame.itemID  = item.itemID
    frame.decorID = item.decorID
    frame.entry   = item.entry
    frame:SetScript("OnClick", function(rowFrame)
        State.selectedItemID = rowFrame.itemID
        if r.itemListScrollBox then
            r.itemListScrollBox:ForEachFrame(function(row)
                if row.selected then row.selected:SetShown(State.selectedItemID == row.itemID) end
            end)
        end
        local ItemPanel = privateDB and privateDB.HDG_ST and privateDB.HDG_ST.ItemPanel
        if ItemPanel then
            ItemPanel:Render(GetScheme())
            ItemPanel:LoadModel(rowFrame.decorID)
        end
    end)
end

function M:RenderItemList(items)
    local r = self._refs
    if not r.itemListScrollBox then return end
    local provider = CreateDataProvider(items)
    r.itemListScrollBox:SetDataProvider(provider, ScrollBoxConstants.RetainScrollPosition)
end

-- =============================================================================
-- MAIN DETAIL RENDER (tier / themed / curated / collection)
-- =============================================================================
-- Dispatches to RenderShoppingList / RenderSnapshot for those branches.
-- Otherwise renders the full chip-row + filter-bar + grid for tier-style
-- detail views.

function M:Render()
    -- Shopping list redirect (different data source, different render path).
    if State.selectedShoppingListId then
        self:RenderShoppingList()
        return
    end

    local SE = GetStyleEngine()
    if not State.selectedStyle or not SE then return end

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

    local r = self._refs
    local scheme = GetScheme()

    -- Header text + colour.
    if r.detailHeader then
        r.detailHeader:SetText(def.displayName)
        if def.color then
            r.detailHeader:SetTextColor(def.color.r, def.color.g, def.color.b, 1)
        end
    end

    -- Type pill (CURATED / SMART SET / SNAPSHOT / COLLECTION / THEMED).
    local tierKey = def.tier
    local hasChipRows = (tierKey ~= "curated" and tierKey ~= "collection" and tierKey ~= "snapshot")
    if r.detailFrame and r.detailFrame.typePill then
        local typeText = ""
        if tierKey == "curated" then         typeText = L["STYLE_TYPE_CURATED"]
        elseif tierKey == "snapshot" then    typeText = L["STYLE_TYPE_SNAPSHOT"]
        elseif tierKey == "collection" then  typeText = L["STYLE_TYPE_COLLECTION"]
        elseif def.subcategories then        typeText = L["STYLE_TYPE_THEMED"]
        else                                 typeText = L["STYLE_TYPE_SMART"]
        end
        r.detailFrame.typePill:SetText(typeText)
    end

    -- Compact layout: curated/collection/snapshot have no chip rows, so slide
    -- the filter bar + grid up under the header. Run BEFORE the snapshot
    -- early-return so transitions land correctly.
    if r.filterBar and r.listFrame and r.cardGridFrame then
        local filterY = hasChipRows and -74 or -36
        local gridY   = hasChipRows and -118 or -64
        r.filterBar:ClearAllPoints()
        r.filterBar:SetPoint("TOPLEFT", 6, filterY)
        r.filterBar:SetPoint("RIGHT", -6, 0)
        r.listFrame:ClearAllPoints()
        r.listFrame:SetPoint("TOPLEFT", 4, gridY)
        r.listFrame:SetPoint("BOTTOMLEFT", 4, 4)
        r.listFrame:SetWidth(280)
        r.cardGridFrame:ClearAllPoints()
        r.cardGridFrame:SetPoint("TOPLEFT", 4, gridY)
        r.cardGridFrame:SetPoint("BOTTOMRIGHT", -4, 4)
    end

    -- Export button: show for custom/curated/snapshot (skip collections).
    local StyleSerializer = privateDB and privateDB.HDG_StyleSerializer
    if r.detailFrame and r.detailFrame.detailExportBtn then
        local showExport = (def.tier == "custom" or def.tier == "curated" or def.tier == "snapshot") and StyleSerializer ~= nil
        r.detailFrame.detailExportBtn:SetShown(showExport)
        if def.tier == "snapshot" then
            r.detailFrame.detailExportBtn:SetScript("OnClick", function()
                local encoded = StyleSerializer:ExportSnapshot(def)
                local StylesTab = privateDB.HDG_StylesTab
                if encoded and StylesTab and StylesTab.ShowExportDialog then
                    StylesTab:ShowExportDialog(encoded, def)
                end
            end)
        else
            r.detailFrame.detailExportBtn:SetScript("OnClick", function()
                local encoded = StyleSerializer:Export(def)
                local StylesTab = privateDB.HDG_StylesTab
                if encoded and StylesTab and StylesTab.ShowExportDialog then
                    StylesTab:ShowExportDialog(encoded)
                end
            end)
        end
    end

    -- Snapshot branch: flat gallery, no chip rows.
    if def.tier == "snapshot" then
        self:RenderSnapshot(def)
        return
    end

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

    local items = SE:GetStyleItems(State.selectedStyle)
    local isSubcat     = def.subcategories ~= nil
    local isCollection = (def.tier == "collection")
    local isCurated    = (def.tier == "curated")

    -- Build hidden-set once so per-tier counters and the filter loop share
    -- the same hide-unreleased semantics.
    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 isCurated or isCollection then
        -- Handpicked sets and collections: no tier/room/subcat chips. Items render flat.
        self:HideFilterButtons()
    elseif isSubcat then
        -- Hide tier chips, show/create subcategory chips.
        for _, btn in pairs(r.tierButtons or {}) do btn:Hide() end
        local scX = 6
        for i, sc in ipairs(def.subcategories) do
            local btn = r.subcatButtons[i]
            if not btn then
                btn = HDG_UIHelpers.CreateChipButton(r.detailFrame, "", 60, false, nil)
                r.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()
                State.selectedTier = sc.key
                State.selectedItemID = nil
                M:Render()
            end)
            HDG_UIHelpers.UpdateChipButtonState(btn, sc.key == State.selectedTier)
            btn:Show()
            scX = scX + btn:GetWidth() + 4
        end
        for j = #def.subcategories + 1, #r.subcatButtons do
            r.subcatButtons[j]:Hide()
        end
    else
        -- Hide subcat buttons, show tier buttons with counts.
        for _, btn in ipairs(r.subcatButtons or {}) do btn:Hide() end
        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(r.tierButtons or {}) do
            btn:Show()
            local label = btn.text:GetText():gsub(" %(%d+%)", "")
            btn.text:SetText(label .. " (" .. (tierCounts[key] or 0) .. ")")
            HDG_UIHelpers.UpdateChipButtonState(btn, key == State.selectedTier)
        end
    end

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

    -- Room/Source chip row.
    if isCollection or isCurated then
        -- already hidden via HideFilterButtons above
    elseif isSubcat then
        -- Source-type chips replace rooms for thematic styles.
        for _, btn in ipairs(r.roomButtons or {}) 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 = r.sourceButtons[i]
            if not btn then
                btn = HDG_UIHelpers.CreateChipButton(r.detailFrame, "", 60, false, nil)
                r.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()
                State.selectedSource = sd.key
                M:Render()
            end)
            HDG_UIHelpers.UpdateChipButtonState(btn, sd.key == State.selectedSource)
            btn:Show()
            srcX = srcX + btn:GetWidth() + 2
        end
        for j = #SOURCE_FILTER_DEFS + 1, #r.sourceButtons do
            r.sourceButtons[j]:Hide()
        end
    else
        -- Room chips for tier styles.
        for _, btn in ipairs(r.sourceButtons or {}) 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 = SE:GetItemRooms(item.itemID)
                if rooms then
                    for _, rk in ipairs(rooms) do
                        roomCounts[rk] = (roomCounts[rk] or 0) + 1
                    end
                end
            end
        end
        local roomX = 6
        for _, btn in ipairs(r.roomButtons or {}) 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 == State.selectedRoom)
            btn:Show()
        end
    end

    -- Subtitle: total visible items + optional descriptor.
    local totalVisible = 0
    for _, item in ipairs(tierItems) do
        if not IsHidden(item.itemID) then totalVisible = totalVisible + 1 end
    end
    local descPiece = (def.description and def.description ~= "") and (" -- " .. def.description) or ""
    if r.detailDesc then
        r.detailDesc:SetText(string.format(L["STYLE_DETAIL_ITEM_COUNT"], totalVisible) .. descPiece)
    end

    -- Build filtered item list (search / collection / craftable / room / source).
    local filtered = {}
    for _, item in ipairs(tierItems) do
        local pass = true

        if pass and IsHidden(item.itemID) then pass = false end

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

        if pass and State.craftableOnly then
            local ic = SE:GetItemInfo(item.itemID)
            if not (ic and ic.isCraftable) then pass = false end
        end

        if pass and State:IsUncollectedOnly() then
            if HDG_Data and HDG_Data.IsItemCollected(item.itemID) then pass = false end
        elseif pass and State:IsCollectedOnly() then
            if not (HDG_Data and HDG_Data.IsItemCollected(item.itemID)) then pass = false end
        end

        if pass and isSubcat and State.selectedSource then
            if State.selectedSource == "rep" then
                if not ItemHasRepRequirement(item.itemID) then pass = false end
            elseif State.selectedSource == "endeavor" then
                if not ItemCostsEndeavor(item.itemID) then pass = false end
            else
                local ic = SE:GetItemInfo(item.itemID)
                if not ic or ic.sourceType ~= State.selectedSource then pass = false end
            end
        elseif pass and not isSubcat and State.selectedRoom then
            local ic = SE:GetItemInfo(item.itemID)
            local rooms = ic and ic.rooms
            if not rooms then
                pass = false
            else
                local found = false
                for _, rk in ipairs(rooms) do
                    if rk == State.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

    M.SortByDecorName(filtered)

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

-- =============================================================================
-- SESSION DETAIL RENDER (recent edit session inside the current house)
-- =============================================================================
-- Read-only view; rents the detail frame to show a flat list of decor that
-- was placed/removed during the session. Items come from HDG.RecentActivity.

function M:RenderSession()
    local sd = State.selectedSessionData
    if not sd then return end
    local houseGUID, sessionID = sd.houseGUID, sd.sessionID
    local displayName = sd.displayName or "Edit Session"

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

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

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

    -- Hide export + view-toggle buttons (read-only view).
    if r.detailFrame then
        if r.detailFrame.detailExportBtn then r.detailFrame.detailExportBtn:Hide() end
        if r.detailFrame.viewToggleBtn   then r.detailFrame.viewToggleBtn:Hide()   end
    end

    self:HideFilterButtons()
    if r.listFrame    then r.listFrame:Hide()     end
    if r.detailPanel  then r.detailPanel:Hide()   end
    if r.cardGridFrame then r.cardGridFrame:Show() end

    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(r.cardGridScrollBox, displayItems)
end

return M
