-- Housing Decor Guide - Acquisition Row Factories & Renderers
-- Row rendering for item / vendor-header / bundle-header rows + 3D gallery cells.
-- Migrated from pooled CreateFrame rows to WowScrollBoxList initializer+bind pattern.
-- The outer ScrollBox in HDG_AcquisitionTab owns the pool; this module just
-- populates frames on first acquire (Initialize*) and re-binds them every time
-- a frame scrolls back into view with new data (Bind*).

local addonName, addon = ...

HDG_AcqRows = HDG_AcqRows or {}

-- Theme helpers (delegating to ThemeEngine canonical helpers)
local HDG = _G.HousingDecorGuide
local function GetScheme() return HDG.Theme.GetCurrentScheme() end
local function RegisterWidget(w, t) HDG.Theme.RegisterWidget(w, t) end
local ApplyFont = function(fs, s, ft) HDG.Theme.ApplyFont(fs, s, ft) end

-- ============================================================================
-- CACHED SCHEME (avoids per-row GetScheme/ApplyFont overhead)
-- ============================================================================

local cachedScheme = nil

function HDG_AcqRows.InvalidateSchemeCache()
    cachedScheme = nil
end

local function EnsureCachedScheme()
    if not cachedScheme then cachedScheme = GetScheme() end
    return cachedScheme
end

-- ============================================================================
-- CACHED DYE ICON
-- ============================================================================

local cachedDyeIconStr

-- Alt-source chip labels — read from CONFIG.SOURCE_TYPES[id].short.
-- Lazy proxy table because CONFIG isn't available at module load.
local ALT_CHIP_LABELS = setmetatable({}, { __index = function(_, id)
    local CONFIG = addon and addon.CONFIG
    local info = CONFIG and CONFIG.SOURCE_TYPES and CONFIG.SOURCE_TYPES[id]
    return info and info.short or nil
end })

local function GetDyeIconStr()
    if not cachedDyeIconStr then
        local icon = C_Item.GetItemIconByID(259113)
        if icon then cachedDyeIconStr = string.format("|T%s:12:12|t", icon) end
    end
    return cachedDyeIconStr
end

-- ============================================================================
-- GALLERY CELL POOL (unchanged from previous version)
-- ============================================================================

local galleryCellPool = { all = {}, active = {}, free = {}, timers = {} }

function HDG_AcqRows.ReleaseAllCells()
    HDG_UIHelpers.ReleaseAllGalleryCells(galleryCellPool)
end

function HDG_AcqRows.UpdateShopListBadge()
    if HDG_UI and HDG_UI.UpdateShopListBadge then HDG_UI.UpdateShopListBadge() end
end

-- ============================================================================
-- EXTENT CALCULATOR (variable-height rows)
-- ============================================================================

function HDG_AcqRows.GetElementExtent(elementData, CONFIG)
    local kind = elementData.kind
    if kind == "vendor" then
        return CONFIG.ACQUISITION_TAB.VENDOR_ROW_HEIGHT
    elseif kind == "bundle" then
        return 18 -- BUNDLE_HEADER_HEIGHT (was inline in HDG_AcquisitionTab)
    else
        return CONFIG.ACQUISITION_TAB.ITEM_ROW_SPACING
    end
end

-- ============================================================================
-- ONE-TIME FRAME INITIALIZER
-- Creates every widget all three row kinds can need, once per frame. Per-bind
-- code only adjusts visibility / text / colors. Widget overhead is ~20 frames
-- total so the "extra" widgets unused by any given kind cost essentially nothing.
-- ============================================================================

function HDG_AcqRows.InitializeRow(frame, CONFIG)
    if frame._hdgInit then return end
    frame._hdgInit = true

    local ACQ = CONFIG.ACQUISITION_TAB
    frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    -- Background texture (replaces SetBackdrop; ScrollBox-created frames are
    -- bare Button frames with no template, so no backdrop methods available)
    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints()

    -- ========================================================================
    -- ITEM row widgets
    -- ========================================================================
    frame.itemNameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.itemNameText:SetJustifyH("LEFT")
    frame.itemNameText:SetWordWrap(false)

    frame.collectedIcon = frame:CreateTexture(nil, "OVERLAY")
    frame.collectedIcon:SetSize(ACQ.STATUS_ICON_SIZE, ACQ.STATUS_ICON_SIZE)
    frame.collectedIcon:SetPoint("RIGHT", -6, 0)
    frame.collectedIcon:SetTexture(CONFIG.TEXTURES.CHECKMARK)

    -- Gate chip (Rep/Q/Ach/Race/etc.) — simple tinted rect with text, no backdrop
    frame.gateChip = CreateFrame("Frame", nil, frame)
    frame.gateChip:SetSize(36, 14)
    frame.gateChip:SetPoint("RIGHT", frame.collectedIcon, "LEFT", -2, 0)
    frame.gateChipBg = frame.gateChip:CreateTexture(nil, "BACKGROUND")
    frame.gateChipBg:SetAllPoints()
    frame.gateChip.text = frame.gateChip:CreateFontString(nil, "OVERLAY", "GameFontHighlightExtraSmall")
    frame.gateChip.text:SetPoint("CENTER", 0, 0)
    frame.gateChip.text:SetJustifyH("CENTER")
    RegisterWidget(frame.gateChip, "Frame")

    frame.statusText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.statusText:SetPoint("RIGHT", frame.gateChip, "LEFT", -2, 0)
    frame.statusText:SetJustifyH("RIGHT")

    -- ========================================================================
    -- VENDOR row widgets
    -- ========================================================================
    frame.collapseBtn = CreateFrame("Button", nil, frame)
    frame.collapseBtn:SetSize(14, 14)
    frame.collapseBtn:SetPoint("LEFT", 4, 0)
    frame.collapseBtn:SetHighlightAtlas("common-button-collapseExpand-hover")

    frame.vendorNameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.vendorNameText:SetPoint("LEFT", frame.collapseBtn, "RIGHT", 2, 0)
    frame.vendorNameText:SetJustifyH("LEFT")
    frame.vendorNameText:SetWordWrap(false)

    frame.vendorCountText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.vendorCountText:SetPoint("RIGHT", -6, 0)
    frame.vendorCountText:SetJustifyH("RIGHT")
    frame.vendorCountText:SetWordWrap(false)

    frame.vendorZoneText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.vendorZoneText:SetPoint("RIGHT", frame.vendorCountText, "LEFT", -4, 0)
    frame.vendorZoneText:SetJustifyH("RIGHT")
    frame.vendorZoneText:SetWordWrap(false)

    -- ========================================================================
    -- BUNDLE row widgets
    -- ========================================================================
    frame.bundleNameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.bundleNameText:SetJustifyH("LEFT")
    frame.bundleNameText:SetWordWrap(false)

    frame.bundlePriceText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.bundlePriceText:SetPoint("RIGHT", frame, "RIGHT", -8, 0)
    frame.bundlePriceText:SetJustifyH("RIGHT")
    frame.bundlePriceText:SetWordWrap(false)

    -- ========================================================================
    -- Shared scripts dispatch on frame._kind
    -- ========================================================================
    frame:SetScript("OnClick", function(self, button)
        if self._kind == "vendor" then
            if IsShiftKeyDown() then
                local ve = HDG_VendorDB and HDG_VendorDB[self._npcID]
                if ve and ve.items and HDG.Store then
                    local addCount = 0
                    for iID, _ in pairs(ve.items) do
                        if HDG.Store:AddToVendorShoppingList(iID, self._npcID) then
                            addCount = addCount + 1
                        end
                    end
                    if addCount > 0 then
                        local vname = HDG_VendorResolver.GetVendorNameByID(self._npcID)
                        print(HDG.Theme.C_Success() .. "[HDG]|r Added " .. "|cFF14b8a6" .. addCount .. "|r items from " .. vname .. " to shopping list")
                        if HDG_VendorShoppingList and HDG_VendorShoppingList.IsShown and HDG_VendorShoppingList.IsShown() then
                            HDG_VendorShoppingList.Refresh()
                        end
                        HDG_AcqRows.UpdateShopListBadge()
                    end
                end
                return
            end
            if self._onToggle then self._onToggle(self._npcID) end
        elseif self._kind == "item" then
            if IsShiftKeyDown() and HDG.Store then
                local npcIDs = HDG_VendorByItem and HDG_VendorByItem[self._itemID]
                local npcID = nil
                if npcIDs and #npcIDs > 0 then
                    npcID = self._parentNpcID or npcIDs[1]
                end
                if HDG.Store:AddToVendorShoppingList(self._itemID, npcID) then
                    local iName = HDG_Data and HDG_Data.GetCachedItemName(self._itemID) or ("Item " .. self._itemID)
                    local dest = npcID and "shopping list" or "wish list"
                    print(HDG.Theme.C_Success() .. "[HDG]|r Added " .. "|cFF14b8a6" .. iName .. "|r to " .. dest)
                    if HDG_VendorShoppingList and HDG_VendorShoppingList.IsShown and HDG_VendorShoppingList.IsShown() then
                        HDG_VendorShoppingList.Refresh()
                    end
                    HDG_AcqRows.UpdateShopListBadge()
                end
                return
            end
            if self._onSelect then self._onSelect(self._itemID, self._parentNpcID) end
        end
    end)

    frame.collapseBtn:SetScript("OnClick", function(self)
        local parent = self:GetParent()
        if parent._kind == "vendor" and parent._onToggle then
            parent._onToggle(parent._npcID)
        end
    end)

    frame:SetScript("OnEnter", function(self)
        if self._kind == "vendor" then
            local c = EnsureCachedScheme()
            if c then self.bg:SetColorTexture(c.button_hover.r, c.button_hover.g, c.button_hover.b, 0.3) end
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(self._displayName or "", 1, 1, 1)
            GameTooltip:AddLine("Shift-Click to add all items to Shopping List", 0.78, 0.78, 0.78)
            GameTooltip:Show()
        elseif self._kind == "item" then
            if self._selectedID ~= self._itemID then
                local c = EnsureCachedScheme()
                if c then self.bg:SetColorTexture(c.button_hover.r, c.button_hover.g, c.button_hover.b, 0.5) end
            end
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if self._itemID then
                local item = Item:CreateFromItemID(self._itemID)
                item:ContinueOnItemLoad(function()
                    if GameTooltip:IsOwned(self) then
                        GameTooltip:SetItemByID(self._itemID)
                        GameTooltip:AddLine(" ")
                        GameTooltip:AddLine("Shift-Click to add to Shopping List", 0.78, 0.78, 0.78)
                        GameTooltip:Show()
                    end
                end)
            end
        end
    end)

    frame:SetScript("OnLeave", function(self)
        if self._kind == "vendor" then
            local c = EnsureCachedScheme()
            if c then self.bg:SetColorTexture(c.bg.r, c.bg.g, c.bg.b, 0.8) end
        elseif self._kind == "item" then
            if self._selectedID ~= self._itemID and self._baseColor then
                self.bg:SetColorTexture(self._baseColor[1], self._baseColor[2], self._baseColor[3], self._baseColor[4])
            end
        end
        GameTooltip:Hide()
    end)
end

-- ============================================================================
-- HIDE ALL KIND-SPECIFIC WIDGETS
-- Called at the start of every bind so widgets from the previous binding (of
-- a possibly different kind) don't leak through.
-- ============================================================================

local function HideAllKindWidgets(frame)
    frame.itemNameText:Hide()
    frame.collectedIcon:Hide()
    frame.gateChip:Hide()
    frame.statusText:Hide()
    frame.collapseBtn:Hide()
    frame.vendorNameText:Hide()
    frame.vendorCountText:Hide()
    frame.vendorZoneText:Hide()
    frame.bundleNameText:Hide()
    frame.bundlePriceText:Hide()
end

-- ============================================================================
-- BIND: ITEM ROW
-- ============================================================================

function HDG_AcqRows.BindItemRow(frame, elementData, CONFIG, ctx)
    local scheme = EnsureCachedScheme()
    local ACQ = CONFIG.ACQUISITION_TAB
    local indent = elementData.indent or 0
    local itemID = elementData.itemID
    local listWidth = frame:GetWidth()
    if listWidth == 0 then listWidth = 200 end

    frame._kind = "item"
    frame._itemID = itemID
    frame._parentNpcID = elementData.npcID
    frame._onSelect = ctx.onSelectItem
    frame._selectedID = ctx.selectedItemID

    HideAllKindWidgets(frame)

    -- Anchor name text (indent-aware); stretch between indent+6 and gateChip
    frame.itemNameText:ClearAllPoints()
    frame.itemNameText:SetPoint("LEFT", indent + 6, 0)
    frame.itemNameText:Show()

    local isCollected = HDG_Data.IsItemCollected(itemID)
    local si0 = HDG_AcqData.GetSourceInfo(itemID)
    local adEntry = HDG_AllDecorDB and HDG_AllDecorDB[itemID]
    local name = HDG_Data.GetCachedItemName(itemID) or ("Item #" .. itemID)

    -- Chip: AllDecorDB alt is primary source; VendorDB enhances with met/unmet
    local npcIDs = HDG_VendorByItem and HDG_VendorByItem[itemID]
    local hasGate, meetsGate, gateType = false, true, nil
    local alt = adEntry and adEntry.alt
    if alt then
        local label = ALT_CHIP_LABELS[alt[1]]
        if label then hasGate = true; gateType = label end
    end
    if npcIDs then
        for _, nID in ipairs(npcIDs) do
            local ve = HDG_VendorDB[nID]
            local ie = ve and ve.items and ve.items[itemID]
            if ie then
                if (ie[2] or 0) ~= 0 then
                    hasGate = true; gateType = gateType or "Rep"
                    if not HDG_VendorResolver.MeetsRepRequirement({ factionID = ie[2], minRep = ie[3] or 0 }) then meetsGate = false end
                end
                if (ie[6] or 0) ~= 0 then
                    if not C_QuestLog.IsQuestFlaggedCompleted(ie[6]) then meetsGate = false end
                end
                if (ie[7] or 0) ~= 0 then
                    local _, _, _, completed = GetAchievementInfo(ie[7])
                    if not completed then meetsGate = false end
                end
            end
            if ve and ve.race then
                hasGate = true; gateType = gateType or "Race"
                if not HDG_VendorResolver.MeetsRaceRequirement({ race = ve.race }) then meetsGate = false end
            end
            if hasGate then break end
        end
    end

    -- Backdrop: selection tint, else zebra-striped panel/bg based on _rowIndex.
    -- Borders were dropped when ScrollBox migration replaced BackdropTemplate, so
    -- zebra provides the row-boundary cue instead.
    local baseColor
    local rowIndex = elementData._rowIndex or 1
    local isSelected = (ctx.selectedItemID == itemID and (not ctx.selectedNpcID or ctx.selectedNpcID == elementData.npcID))
    if scheme then
        if isSelected then
            baseColor = { scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3 }
        elseif rowIndex % 2 == 0 then
            baseColor = { scheme.panel.r, scheme.panel.g, scheme.panel.b, scheme.panel.a }
        else
            baseColor = { scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.6 }
        end
        ApplyFont(frame.itemNameText, scheme, "small")
    else
        baseColor = (rowIndex % 2 == 0) and { 0.1, 0.1, 0.12, 0.6 } or { 0.08, 0.08, 0.1, 0.6 }
    end
    frame._baseColor = baseColor
    frame.bg:ClearAllPoints()
    frame.bg:SetPoint("TOPLEFT", indent, 0)
    frame.bg:SetPoint("BOTTOMRIGHT", 0, 0)
    frame.bg:SetColorTexture(baseColor[1], baseColor[2], baseColor[3], baseColor[4])

    -- Source icon prefix
    local displayName = name
    local si = si0
    local siType = si and si[2] or 0
    local isCraftable = HDG_AcqData.craftableByItem[itemID] ~= nil
    if si and HDG_AcqData.IsAcquirableType(siType) then
        local iconRef = HDG_AcqData.GetSourceTypeAtlas(siType, CONFIG)
        if iconRef then
            if iconRef:find("^TEXTURE:") then
                displayName = string.format("|T%s:14:14|t", iconRef:sub(9)) .. " " .. name
            else
                displayName = CreateAtlasMarkup(iconRef, 14, 14) .. " " .. name
            end
        end
    elseif isCraftable then
        displayName = string.format("|T%s:14:14|t", CONFIG.TEXTURES.CRAFTED_ICON) .. " " .. name
    end

    local StyleEngine = addon.HDG_StyleEngine
    if StyleEngine and StyleEngine:IsDyeable(itemID) then
        local dyeStr = GetDyeIconStr()
        if dyeStr then displayName = displayName .. " " .. dyeStr end
    end

    if HDG and HDG.Store and HDG.Store:GetUserNote(itemID) then
        displayName = displayName .. " " .. HDG.Theme.C_Dim() .. "[N]|r"
    end

    local ST = CONFIG.SOURCE_TYPE
    local hasStatus = (si and HDG_AcqData.IsAcquirableType(siType) and (siType == ST.ACHIEVEMENT or siType == ST.QUEST or siType == ST.WORLD_QUEST or siType == ST.DROP or siType == ST.CRAFTED or siType == ST.TREASURE or siType == ST.PROMOTION or siType == ST.SHOP)) or (isCraftable and not (si and HDG_AcqData.IsAcquirableType(siType)))
    frame.itemNameText:SetWidth(listWidth - indent - ((hasStatus or hasGate) and 110 or 30))
    frame.itemNameText:SetText(displayName)
    if scheme then
        if isCollected then
            frame.itemNameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
        else
            frame.itemNameText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
        end
    end

    if isCollected then
        frame.collectedIcon:SetVertexColor(0.2, 0.9, 0.2)
        frame.collectedIcon:Show()
    else
        frame.collectedIcon:Hide()
    end

    if hasGate and gateType then
        local cs = HDG.Theme.currentScheme or scheme
        -- Look up source-type color by `short` field (e.g. "Ach"→Achievement gold,
        -- "Rep"→slate violet). Non-source gates (Race) fall back to status color.
        local CONFIG = addon and addon.CONFIG
        local typeColor
        if CONFIG and CONFIG.SOURCE_TYPES then
            for _, info in pairs(CONFIG.SOURCE_TYPES) do
                if info.short == gateType and info.color then
                    typeColor = { r = info.color[1], g = info.color[2], b = info.color[3] }
                    break
                end
            end
        end
        local fallbackClr = (isCollected or meetsGate) and cs.success or cs.warning
        local clr = typeColor or fallbackClr
        -- Met: full alpha 0.85; unmet: dimmed 0.45 to keep the "blocked" cue.
        local met = (isCollected or meetsGate)
        local bgAlpha = met and 0.85 or 0.45
        frame.gateChipBg:SetColorTexture(clr.r * 0.35, clr.g * 0.35, clr.b * 0.35, bgAlpha)
        frame.gateChip.text:SetText(gateType)
        frame.gateChip.text:SetTextColor(clr.r, clr.g, clr.b, met and 1.0 or 0.55)
        frame.gateChip:Show()
    else
        frame.gateChip:Hide()
    end

    if hasStatus and not hasGate then
        if scheme then ApplyFont(frame.statusText, scheme, "xsmall") end
        if isCollected then
            frame.statusText:SetText("Done")
            frame.statusText:SetTextColor(0.2, 0.9, 0.2)
        else
            frame.statusText:SetText("Incomplete")
            frame.statusText:SetTextColor(0.9, 0.4, 0.2)
        end
        frame.statusText:Show()
    else
        frame.statusText:Hide()
    end
end

-- ============================================================================
-- BIND: VENDOR HEADER ROW
-- ============================================================================

function HDG_AcqRows.BindVendorRow(frame, elementData, CONFIG, ctx)
    local scheme = EnsureCachedScheme()
    local ACQ = CONFIG.ACQUISITION_TAB
    local npcID = elementData.npcID
    local ve = HDG_VendorDB and HDG_VendorDB[npcID]
    if not ve then return end
    local listWidth = frame:GetWidth()
    if listWidth == 0 then listWidth = 200 end

    frame._kind = "vendor"
    frame._npcID = npcID
    frame._itemID = nil
    frame._parentNpcID = nil
    frame._onToggle = ctx.onVendorToggle

    HideAllKindWidgets(frame)
    frame.collapseBtn:Show()
    frame.vendorNameText:Show()
    frame.vendorCountText:Show()
    frame.vendorZoneText:Show()

    frame.bg:ClearAllPoints()
    frame.bg:SetAllPoints()
    if scheme then
        frame.bg:SetColorTexture(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.8)
        ApplyFont(frame.vendorNameText, scheme, "small")
        frame.vendorNameText:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
        frame.vendorNameText:SetWidth(listWidth * 0.55)
        ApplyFont(frame.vendorCountText, scheme, "small")
        frame.vendorCountText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
        ApplyFont(frame.vendorZoneText, scheme, "xsmall")
        frame.vendorZoneText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
        frame.vendorZoneText:SetWidth(listWidth * 0.30)
    end

    local isExpanded = ctx.expandedVendors and ctx.expandedVendors[npcID]
    if isExpanded then
        frame.collapseBtn:SetNormalAtlas("common-button-collapseExpand-down")
        frame.collapseBtn:SetPushedAtlas("common-button-collapseExpand-down-pressed")
    else
        frame.collapseBtn:SetNormalAtlas("common-button-collapseExpand-up")
        frame.collapseBtn:SetPushedAtlas("common-button-collapseExpand-up-pressed")
    end
    frame.vendorZoneText:SetText(ve[2] or "")

    local vendorFaction = ve[6] or "N"
    local factionPrefix = ""
    if vendorFaction == "A" then
        factionPrefix = CreateAtlasMarkup("communities-create-button-wow-alliance", 14, 14) .. " "
    elseif vendorFaction == "H" then
        factionPrefix = CreateAtlasMarkup("communities-create-button-wow-horde", 14, 14) .. " "
    end
    local vendorDisplayName = factionPrefix .. HDG_VendorResolver.GetVendorNameByID(npcID)
    if HDG and HDG.Store and HDG.Store:GetVendorNote(npcID) then
        vendorDisplayName = vendorDisplayName .. " " .. HDG.Theme.C_Dim() .. "[N]|r"
    end
    frame.vendorNameText:SetText(vendorDisplayName)
    frame._displayName = vendorDisplayName

    local counts = HDG_AcqData.vendorCounts[npcID]
    local total = counts and counts.total or 0
    local collected = counts and counts.collected or 0
    frame.vendorCountText:SetText(string.format("(%d/%d)", collected, total))
end

-- ============================================================================
-- BIND: BUNDLE HEADER ROW (Shop vendor only)
-- ============================================================================

function HDG_AcqRows.BindBundleRow(frame, elementData, CONFIG)
    local bundleMeta = elementData.bundle
    if not bundleMeta then return end
    local indent = elementData.indent or 0

    frame._kind = "bundle"
    frame._itemID = nil
    frame._parentNpcID = nil

    HideAllKindWidgets(frame)
    frame.bundleNameText:ClearAllPoints()
    frame.bundleNameText:SetPoint("LEFT", indent + 4, 0)
    frame.bundleNameText:SetPoint("RIGHT", frame.bundlePriceText, "LEFT", -8, 0)
    frame.bundleNameText:Show()
    frame.bundlePriceText:Show()

    frame.bg:ClearAllPoints()
    frame.bg:SetPoint("TOPLEFT", indent, 0)
    frame.bg:SetPoint("BOTTOMRIGHT", 0, 0)
    frame.bg:SetColorTexture(0, 0, 0, 0) -- bundle row has no bg tint (matches prior render)

    local t = HDG.Theme.C_Text()
    local hsIcon = "|A:hearthsteel-icon-32x32:12:12|a"
    frame.bundleNameText:SetText(t .. bundleMeta.name)
    local priceStr
    if bundleMeta.cost then
        priceStr = t .. tostring(bundleMeta.cost) .. " " .. hsIcon
    elseif bundleMeta.moneyCost and bundleMeta.moneyCost ~= "" then
        priceStr = t .. bundleMeta.moneyCost
    else
        priceStr = "|cFFFFD54F$$$|r"
    end
    frame.bundlePriceText:SetText(priceStr)
end

-- ============================================================================
-- DISPATCH BIND (called from initializer)
-- ============================================================================

function HDG_AcqRows.BindRow(frame, elementData, CONFIG, ctx)
    local kind = elementData.kind
    if kind == "vendor" then
        HDG_AcqRows.BindVendorRow(frame, elementData, CONFIG, ctx)
    elseif kind == "bundle" then
        HDG_AcqRows.BindBundleRow(frame, elementData, CONFIG)
    else
        HDG_AcqRows.BindItemRow(frame, elementData, CONFIG, ctx)
    end
end

-- ============================================================================
-- SELECTION HIGHLIGHT REPAINT (scrollBox:ForEachFrame)
-- Replaces UpdateRowHighlights which iterated the old rowPool.
-- ============================================================================

function HDG_AcqRows.UpdateRowHighlights(scrollBox, selectedItemID, selectedNpcID)
    if not scrollBox or not scrollBox.ForEachFrame then return end
    local scheme = GetScheme()
    if not scheme then return end
    scrollBox:ForEachFrame(function(frame, elementData)
        if frame._kind == "item" then
            local baseColor
            local rowIndex = elementData._rowIndex or 1
            if selectedItemID == frame._itemID and (not selectedNpcID or selectedNpcID == frame._parentNpcID) then
                baseColor = { scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3 }
            elseif rowIndex % 2 == 0 then
                baseColor = { scheme.panel.r, scheme.panel.g, scheme.panel.b, scheme.panel.a }
            else
                baseColor = { scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.6 }
            end
            frame._baseColor = baseColor
            frame._selectedID = selectedItemID
            frame.bg:SetColorTexture(baseColor[1], baseColor[2], baseColor[3], baseColor[4])
        end
    end)
end

-- ============================================================================
-- CREATE GALLERY CELL (3D model) — unchanged from pre-migration
-- ============================================================================

function HDG_AcqRows.CreateGalleryCell(parent, itemID, x, y, cellSize, selectedID, onSelect, CONFIG, npcID)
    local ACQ = CONFIG.ACQUISITION_TAB

    local cell = HDG_UIHelpers.CreateGalleryCell(galleryCellPool, parent, cellSize, {
        statusIconSize = ACQ.STATUS_ICON_SIZE,
        checkmarkTexture = CONFIG.TEXTURES.CHECKMARK,
    })
    cell:SetPoint("TOPLEFT", x, y)

    local isCollected = HDG_Data.IsItemCollected(itemID)
    local isSelected = (selectedID == itemID)
    HDG_UIHelpers.UpdateGalleryCellBackground(cell, isSelected)
    cell.checkmark:SetShown(isCollected)
    cell.redX:SetShown(not isCollected)

    cell.itemID = itemID
    cell.parentNpcID = npcID

    local cellRef = cell
    local loadDelay = #galleryCellPool.timers * 0.05
    local timer = C_Timer.NewTimer(loadDelay, function()
        if not cellRef.inUse or cellRef.itemID ~= itemID then return end
        local decorInfo = HDG_Data and HDG_Data.GetCatalogByItem and HDG_Data.GetCatalogByItem(itemID)
        if not decorInfo then return end
        local sceneID = (decorInfo and decorInfo.uiModelSceneID) or ACQ.DEFAULT_SCENE_ID
        HDG_UIHelpers.UpdateGalleryCellVisuals(cellRef, decorInfo, isCollected, sceneID)
        if decorInfo and decorInfo.recordID then
            HDG_UIHelpers.UpdateGalleryCellQuantity(cellRef, decorInfo.recordID)
        end
    end)
    table.insert(galleryCellPool.timers, timer)

    cell:SetScript("OnClick", function(self)
        if IsShiftKeyDown() and HDG.Store then
            local nIDs = HDG_VendorByItem and HDG_VendorByItem[self.itemID]
            local nID = nil
            if nIDs and #nIDs > 0 then
                nID = self.parentNpcID or nIDs[1]
            end
            if HDG.Store:AddToVendorShoppingList(self.itemID, nID) then
                local iName = HDG_Data and HDG_Data.GetCachedItemName(self.itemID) or ("Item " .. self.itemID)
                local dest = nID and "shopping list" or "wish list"
                print(HDG.Theme.C_Success() .. "[HDG]|r Added " .. "|cFF14b8a6" .. iName .. "|r to " .. dest)
                if HDG_VendorShoppingList and HDG_VendorShoppingList.IsShown and HDG_VendorShoppingList.IsShown() then
                    HDG_VendorShoppingList.Refresh()
                end
                HDG_AcqRows.UpdateShopListBadge()
            end
            return
        end
        if onSelect then onSelect(self.itemID, self.parentNpcID) end
    end)

    if not cell._handlersSet then
        cell:SetScript("OnEnter", function(self)
            self.hoverBg:Show()
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            local tipName = HDG_Data.GetCachedItemName(self.itemID) or ("Item #" .. self.itemID)
            local tipCollected = HDG_Data.IsItemCollected(self.itemID)
            local tipNpcIDs = HDG_VendorByItem and HDG_VendorByItem[self.itemID]
            local tipPrice = 0
            if tipNpcIDs and tipNpcIDs[1] and HDG_VendorDB then
                local ve = HDG_VendorDB[tipNpcIDs[1]]
                local ie = ve and ve.items and ve.items[self.itemID]
                tipPrice = ie and ie[1] or 0
            end
            local priceStr = HDG_VendorResolver.FormatGoldCost(tipPrice)

            GameTooltip:AddLine(tipName, 1, 1, 1)
            if tipCollected then
                GameTooltip:AddLine(HDG.Theme.C_Success() .. "Collected|r", 0.2, 0.9, 0.2)
            else
                GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Not collected|r", 0.78, 0.78, 0.78)
            end
            if priceStr then
                GameTooltip:AddLine("Price: " .. priceStr, 0.8, 0.8, 0.8)
            else
                local currencies = HDG_VendorResolver.GetCurrencyCost(self.itemID)
                if currencies then
                    local iconSize = CONFIG.ACQUISITION_TAB.CURRENCY_ICON_SIZE
                    for _, cc in ipairs(currencies) do
                        local iconStr = HDG_VendorResolver.FormatCurrencyIcon(cc.icon, iconSize)
                        if iconStr ~= "" then iconStr = iconStr .. " " end
                        GameTooltip:AddDoubleLine(iconStr .. (cc.name or "Currency"), tostring(cc.amount), 0.8, 0.8, 0.8, 1, 1, 1)
                    end
                else
                    GameTooltip:AddLine("|cFFAAAA00Visit vendor to see cost|r", 0.7, 0.7, 0.5)
                end
            end
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Shift-Click to add to Shopping List", 0.78, 0.78, 0.78)
            GameTooltip:Show()
        end)
        cell:SetScript("OnLeave", function(self)
            self.hoverBg:Hide()
            GameTooltip:Hide()
        end)
        cell._handlersSet = true
    end

    cell:Show()
    return cell
end

-- ============================================================================
-- HIGHLIGHT-ONLY UPDATES (gallery unchanged)
-- ============================================================================

function HDG_AcqRows.UpdateGalleryHighlights(selectedItemID)
    for _, cell in ipairs(galleryCellPool.active) do
        if not cell.inUse then break end
        HDG_UIHelpers.UpdateGalleryCellBackground(cell, cell.itemID == selectedItemID)
    end
end
