local _, privateDB = ...

-- ============================================================================
-- REQUIRED MATERIALS PANEL (right column of the Recipes tab)
-- Owns the materials ScrollBox + sort-mode + Direct/Raw toggle + cost rollup
-- + "Add All to Auctionator" CTA. Extracted from HDG_RecipesTab.lua during
-- the v2.42.x code-smell sweep.
-- ============================================================================

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

local HDG_MaterialsPanel = {}
privateDB.HDG_MaterialsPanel = HDG_MaterialsPanel

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 function Data() return HDG_UIHelpers.Data() end

local function GetRecipeByItemID(itemID)
    if HDG_PowerCrafting and HDG_PowerCrafting.GetRecipeByItemID then
        return HDG_PowerCrafting.GetRecipeByItemID(itemID)
    end
    return nil
end

local mainFrame = nil

local function UpdateCostDisplay(totalCost, missingPrices)
    if not mainFrame.materialsCostText then return end
    local hasPriceAddon, priceAddonName = Data().HasPriceAddon()
    if not hasPriceAddon then
        mainFrame.materialsCostText:SetText(HDG.Theme.C_Dim() .. (L["STATUS_INSTALL_PRICE_ADDON"] or "Install TSM/Auctionator for price data") .. "|r")
        return
    end
    local costStr = Data().FormatGold(totalCost)
    if missingPrices > 0 then
        mainFrame.materialsCostText:SetText(string.format(
            HDG.Theme.C_Text() .. (L["RECIPE_EST_COST"] or "Est. Cost:") .. "|r " .. HDG.Theme.C_Warning() .. "~%s+|r " .. HDG.Theme.C_Dim() .. "(%d unknown)|r", costStr, missingPrices))
    elseif totalCost > 0 then
        mainFrame.materialsCostText:SetText(string.format(
            HDG.Theme.C_Text() .. (L["RECIPE_EST_COST"] or "Est. Cost:") .. "|r " .. HDG.Theme.C_Warning() .. "~%s|r " .. HDG.Theme.C_Dim() .. "(via %s)|r", costStr, priceAddonName))
    else
        mainFrame.materialsCostText:SetText(HDG.Theme.C_Text() .. (L["RECIPE_EST_COST"] or "Est. Cost:") .. "|r " .. HDG.Theme.C_Success() .. (L["STATUS_HAVE_ALL_MATERIALS"] or "Have all materials!") .. "|r")
    end
end

-- ============================================================================
-- MATERIALS SCROLLBOX
-- Element kinds (one Button frame template, kind-dispatched on bind):
--   matHeader          - collapsible category / recipe / intermediates header
--   matSubHeader       - non-collapsible category sub-divider (ByRecipe mode)
--   matRow             - reagent row with progress StatusBar + clickable area
--   matIntermediate    - intermediate parent row (Totals + Raw mode)
--   matChildRow        - intermediate child reagent row (indented)
--   spacer             - trailing gap
--   emptyState         - "queue empty" message when no content
-- ============================================================================

local MAT_ELEMENT_HEIGHTS = {
    matHeader       = (CONFIG.SPACING and CONFIG.SPACING.HEADER_HEIGHT or 24) + 2,
    matSubHeader    = 20,
    matRow          = (CONFIG.MATERIAL_ITEM_HEIGHT or 22) + 1,
    matIntermediate = (CONFIG.MATERIAL_ITEM_HEIGHT or 22) + 1,
    matChildRow     = (CONFIG.MATERIAL_ITEM_HEIGHT or 22) - 1,
    spacer          = 6,
    emptyState      = 80,
}

local function GetMaterialsElementExtent(_, data)
    if data.kind == "spacer" then return data.height or 6 end
    if data.kind == "matHeader" and data.isRecipeHeader then
        return (CONFIG.SPACING and CONFIG.SPACING.HEADER_HEIGHT or 24) + 4
    end
    return MAT_ELEMENT_HEIGHTS[data.kind] or 22
end

local function HideAllMaterialsWidgets(frame)
    if frame.matCollapseIcon then frame.matCollapseIcon:Hide() end
    if frame.matIcon then frame.matIcon:Hide() end
    if frame.matHeaderText then frame.matHeaderText:Hide() end
    if frame.matHeaderQtyText then frame.matHeaderQtyText:Hide() end
    if frame.matSubIcon then frame.matSubIcon:Hide() end
    if frame.matSubText then frame.matSubText:Hide() end
    if frame.matNameText then frame.matNameText:Hide() end
    if frame.matCountText then frame.matCountText:Hide() end
    if frame.matProfIcon then frame.matProfIcon:Hide() end
    if frame.matProgressBar then frame.matProgressBar:Hide() end
    if frame.matEmptyText then frame.matEmptyText:Hide() end
end

local function InitializeMaterialsFrame(frame)
    if frame._hdgMatInit then return end
    frame._hdgMatInit = true

    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints()

    frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    frame:EnableMouse(true)

    -- matHeader widgets
    frame.matCollapseIcon = CreateFrame("Button", nil, frame)
    frame.matCollapseIcon:SetSize(14, 14)
    frame.matCollapseIcon:SetPoint("LEFT", 4, 0)
    frame.matCollapseIcon:SetHighlightAtlas("common-button-collapseExpand-hover")
    frame.matCollapseIcon:SetScript("OnClick", function()
        if frame._matCollapseKey then
            HDG.Store:Dispatch("SET_COLLAPSED_MATERIAL", { category = frame._matCollapseKey, collapsed = not frame._matIsCollapsed })
            HDG_MaterialsPanel.Refresh()
        end
    end)

    frame.matIcon = frame:CreateTexture(nil, "ARTWORK")
    frame.matIcon:SetSize(14, 14)

    frame.matHeaderText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.matHeaderText:SetJustifyH("LEFT")
    frame.matHeaderText:SetWordWrap(false)

    frame.matHeaderQtyText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.matHeaderQtyText:SetPoint("RIGHT", -8, 0)

    -- matSubHeader widgets
    frame.matSubIcon = frame:CreateTexture(nil, "ARTWORK")
    frame.matSubIcon:SetSize(12, 12)
    frame.matSubText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.matSubText:SetJustifyH("LEFT")
    frame.matSubText:SetWordWrap(false)

    -- matRow / matIntermediate / matChildRow widgets
    frame.matProgressBar = CreateFrame("StatusBar", nil, frame)
    frame.matProgressBar:SetAllPoints()
    frame.matProgressBar:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    frame.matProgressBar:SetFrameLevel(frame:GetFrameLevel())

    frame.matProfIcon = frame:CreateTexture(nil, "ARTWORK")
    frame.matProfIcon:SetSize(14, 14)

    frame.matNameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.matNameText:SetJustifyH("LEFT")
    frame.matNameText:SetWordWrap(false)

    frame.matCountText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.matCountText:SetJustifyH("RIGHT")

    -- emptyState widgets
    frame.matEmptyText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.matEmptyText:SetPoint("CENTER", 0, 0)
    frame.matEmptyText:SetJustifyH("CENTER")

    frame:SetScript("OnClick", function(self, button)
        if self._kind == "matHeader" then
            if self._matCollapseKey then
                HDG.Store:Dispatch("SET_COLLAPSED_MATERIAL", { category = self._matCollapseKey, collapsed = not self._matIsCollapsed })
                HDG_MaterialsPanel.Refresh()
            end
        elseif self._kind == "matRow" and self._matClicksEnabled then
            local mat = self._mat
            local itemName = self._matName
            if not mat or not itemName then return end
            if button == "LeftButton" and IsShiftKeyDown() then
                if HDG_LumberTracker and HDG_LumberTracker.TrackReagent then
                    HDG_LumberTracker.TrackReagent(mat.id, itemName, mat.needed)
                end
            elseif button == "LeftButton" then
                if not AuctionHouseFrame or not AuctionHouseFrame:IsShown() then
                    print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Please open the Auction House first")
                elseif AuctionHouseFrame.SearchBar and AuctionHouseFrame.SearchBar.SearchBox then
                    AuctionHouseFrame.SearchBar.SearchBox:SetText(itemName)
                    print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Set AH search text to:", itemName)
                end
            elseif button == "RightButton" then
                if C_AddOns.IsAddOnLoaded("Auctionator") and Auctionator and Auctionator.API and Auctionator.API.v1 then
                    local callerID = "HousingDecorGuide"
                    local listName = "Housing Decor"
                    local existingItems = {}
                    if Auctionator.API.v1.GetCraftingQueueItems then
                        local success, items = pcall(Auctionator.API.v1.GetCraftingQueueItems, callerID, listName)
                        if success and items then existingItems = items end
                    end
                    local exists = false
                    for _, existingItem in ipairs(existingItems) do
                        if existingItem == itemName then exists = true; break end
                    end
                    if not exists then table.insert(existingItems, itemName) end
                    if Auctionator.API.v1.CreateShoppingList then
                        Auctionator.API.v1.CreateShoppingList(callerID, listName, existingItems)
                        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Added '" .. itemName .. "' to Auctionator search list: " .. listName)
                    end
                else
                    print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Please install Auctionator for search list integration")
                end
            end
        end
    end)

    frame:SetScript("OnEnter", function(self)
        if self._matHoverBgColor then
            local c = self._matHoverBgColor
            self.bg:SetColorTexture(c[1], c[2], c[3], c[4] or 1)
        end
        if self._kind == "matHeader" and self._matIntermediatesTooltip then
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:AddLine(HDG.Theme.C_Accent() .. L["RECIPE_TIP_CRAFT_FIRST"] .. "|r", 1, 1, 1)
            GameTooltip:AddLine(L["RECIPE_TIP_CRAFT_FIRST_1"], 0.7, 0.7, 0.7)
            GameTooltip:AddLine(L["RECIPE_TIP_CRAFT_FIRST_2"], 0.7, 0.7, 0.7)
            GameTooltip:Show()
        elseif self._kind == "matRow" and self._mat then
            local mat = self._mat
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:SetItemByID(mat.id)
            GameTooltip:AddLine(" ")
            if self._matMode == "byrecipe" then
                GameTooltip:AddLine(HDG.Theme.C_Warning() .. "For: " .. (self._matRecipeName or "") .. " x" .. (self._matRecipeQty or 1) .. "|r", 0.9, 0.9, 0.9)
                GameTooltip:AddLine("Per craft: x" .. (mat.perCraft or 1), 0.7, 0.7, 0.7)
            else
                GameTooltip:AddLine("Used in:", 1, 1, 1)
                if mat.sources then
                    for recipeName, data in pairs(mat.sources) do
                        GameTooltip:AddDoubleLine(recipeName, string.format("%d (x%d)", data.count, data.perCraft), 0.8, 0.8, 0.8, 1, 1, 1)
                    end
                end
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine(HDG.Theme.C_Success() .. "Click|r to set AH search text", 0.7, 0.7, 0.7)
                GameTooltip:AddLine(HDG.Theme.C_Success() .. "Shift-Click|r to track reagent", 0.7, 0.7, 0.7)
                GameTooltip:AddLine(HDG.Theme.C_Success() .. "Right-Click|r to add to Auctionator search list", 0.7, 0.7, 0.7)
            end
            GameTooltip:Show()
        elseif self._kind == "matIntermediate" and self._matInter then
            local inter = self._matInter
            local interRecipe = self._matInterRecipe
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:SetItemByID(inter.id)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Craft " .. inter.needed .. "x|r", 1, 1, 1)
            if HDG_PowerCrafting and HDG_PowerCrafting.GetAllRecipesForItem then
                local allRecipes = HDG_PowerCrafting.GetAllRecipesForItem(inter.id)
                if #allRecipes > 0 then
                    local preferred = allRecipes[1]
                    if preferred then
                        local profColor = HDG.Theme.C_Warning()
                        GameTooltip:AddLine(" ")
                        GameTooltip:AddLine("Via: " .. (preferred.name or "Unknown") .. " " .. profColor .. "(" .. (preferred.profession or "?") .. ")|r", 0.9, 0.9, 0.9)
                        if preferred.slots then
                            for _, slot in ipairs(preferred.slots) do
                                if slot.type == "basic" and slot.itemID then
                                    local matN = slot.name or HDG_PowerCrafting.GetItemName(slot.itemID) or ("Item #" .. slot.itemID)
                                    GameTooltip:AddDoubleLine("  " .. matN, "x" .. (slot.qty or 1), 0.7, 0.7, 0.7, 0.8, 0.8, 0.8)
                                end
                            end
                        end
                    end
                    if #allRecipes > 1 then
                        GameTooltip:AddLine(" ")
                        GameTooltip:AddLine(L["RECIPE_TIP_ALT_RECIPES"], 0.6, 0.8, 0.6)
                        for i = 2, #allRecipes do
                            local alt = allRecipes[i]
                            GameTooltip:AddLine("  " .. (alt.name or "Unknown") .. " " .. HDG.Theme.C_Warning() .. "(" .. (alt.profession or "?") .. ")|r", 0.6, 0.6, 0.6)
                        end
                    end
                end
            elseif inter.children and next(inter.children) then
                GameTooltip:AddLine(L["RECIPE_TIP_REQUIRES"], 0.7, 0.7, 0.7)
                for _, childData in pairs(inter.children) do
                    if childData.source ~= "Ignore" then
                        local totalNeeded = childData.perCraft * inter.needed
                        GameTooltip:AddDoubleLine("  " .. childData.name, "x" .. totalNeeded, 0.8, 0.8, 0.8, 1, 1, 1)
                    end
                end
            end
            if interRecipe and interRecipe.recipeID then
                local knowers = HDG_Data.GetCharactersWhoKnowRecipe(interRecipe.recipeID, interRecipe.profession)
                if knowers and #knowers > 0 then
                    local names = {}
                    for _, k in ipairs(knowers) do table.insert(names, k.name) end
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine("Known by: " .. table.concat(names, ", "), 0.4, 0.8, 0.4)
                end
            end
            GameTooltip:Show()
        elseif self._kind == "matChildRow" and self._matChildTooltipID then
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            if type(self._matChildTooltipID) == "number" and self._matChildTooltipID > 0 then
                GameTooltip:SetItemByID(self._matChildTooltipID)
            else
                GameTooltip:SetText(self._matChildName or "Unknown Item", 1, 1, 1)
            end
            GameTooltip:Show()
        end
    end)

    frame:SetScript("OnLeave", function(self)
        if self._matBgColor then
            local c = self._matBgColor
            self.bg:SetColorTexture(c[1], c[2], c[3], c[4] or 1)
        end
        GameTooltip:Hide()
    end)
end

-- ============================================================================
-- BIND helpers (per element kind)
-- ============================================================================

local function BindMatHeaderKind(frame, data, scheme)
    frame._kind = "matHeader"
    frame._matCollapseKey = data.collapseKey
    frame._matIsCollapsed = data.isCollapsed
    frame._matIntermediatesTooltip = data.showIntermediatesTooltip

    local bg = data.bgColor or { scheme.panel.r, scheme.panel.g, scheme.panel.b, 1.0 }
    local hover = data.hoverBgColor or { scheme.button_hover.r, scheme.button_hover.g, scheme.button_hover.b, 1.0 }
    frame._matBgColor = bg
    frame._matHoverBgColor = hover
    frame.bg:SetColorTexture(bg[1], bg[2], bg[3], bg[4] or 1)

    frame.matCollapseIcon:Show()
    if data.isCollapsed then
        frame.matCollapseIcon:SetNormalAtlas("common-button-collapseExpand-up")
        frame.matCollapseIcon:SetPushedAtlas("common-button-collapseExpand-up-pressed")
    else
        frame.matCollapseIcon:SetNormalAtlas("common-button-collapseExpand-down")
        frame.matCollapseIcon:SetPushedAtlas("common-button-collapseExpand-down-pressed")
    end

    if data.icon then
        frame.matIcon:ClearAllPoints()
        frame.matIcon:SetPoint("LEFT", 20, 0)
        frame.matIcon:SetTexture(data.icon)
        frame.matIcon:Show()
        frame.matHeaderText:ClearAllPoints()
        frame.matHeaderText:SetPoint("LEFT", 40, 0)
    else
        frame.matHeaderText:ClearAllPoints()
        frame.matHeaderText:SetPoint("LEFT", 22, 0)
    end

    ApplyFont(frame.matHeaderText, scheme, "small")
    frame.matHeaderText:SetWidth(data.textWidth or 170)
    frame.matHeaderText:SetText(data.text or "")
    if data.textColor then
        frame.matHeaderText:SetTextColor(data.textColor.r, data.textColor.g, data.textColor.b, data.textColor.a or 1)
    elseif scheme.text_header then
        frame.matHeaderText:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, 1)
    end
    frame.matHeaderText:Show()

    if data.qtyText then
        ApplyFont(frame.matHeaderQtyText, scheme, "small")
        frame.matHeaderQtyText:SetText(data.qtyText)
        frame.matHeaderQtyText:Show()
    end
end

local function BindMatSubHeaderKind(frame, data, scheme)
    frame._kind = "matSubHeader"
    local panel = scheme.panel
    frame._matBgColor = { panel.r, panel.g, panel.b, 0.7 }
    frame.bg:SetColorTexture(panel.r, panel.g, panel.b, 0.7)

    if data.icon then
        frame.matSubIcon:ClearAllPoints()
        frame.matSubIcon:SetPoint("LEFT", 12, 0)
        frame.matSubIcon:SetTexture(data.icon)
        frame.matSubIcon:Show()
        frame.matSubText:ClearAllPoints()
        frame.matSubText:SetPoint("LEFT", 28, 0)
    else
        frame.matSubText:ClearAllPoints()
        frame.matSubText:SetPoint("LEFT", 12, 0)
    end
    ApplyFont(frame.matSubText, scheme, "small")
    frame.matSubText:SetText(HDG.Theme.C_Dim() .. (data.text or "") .. "|r")
    frame.matSubText:Show()
end

local function BindMatRowKind(frame, data, scheme)
    local mat = data.mat
    frame._kind = "matRow"
    frame._mat = mat
    frame._matMode = data.mode or "totals"
    frame._matRecipeName = data.recipeName
    frame._matRecipeQty = data.recipeQty
    frame._matClicksEnabled = (data.mode ~= "byrecipe")

    local expColor = HDG_ExpansionData.GetColor(mat.expansion)
    local bgAlpha = data.mode == "byrecipe" and 0.4 or 0.5
    local bg
    if expColor then
        bg = { expColor[1], expColor[2], expColor[3], 0.15 }
    else
        bg = { scheme.panel.r, scheme.panel.g, scheme.panel.b, bgAlpha }
    end
    frame._matBgColor = bg
    frame._matHoverBgColor = bg
    frame.bg:SetColorTexture(bg[1], bg[2], bg[3], bg[4])

    frame.matProgressBar:Show()
    frame.matProgressBar:SetMinMaxValues(0, mat.needed)
    frame.matProgressBar:SetValue(math.min(mat.have, mat.needed))
    local pct = mat.needed > 0 and (mat.have / mat.needed * 100) or 0
    frame.matProgressBar:SetStatusBarColor(HDG_UIHelpers.GetProgressBarColor(pct))

    local matName = Data().GetCachedItemName(mat.id) or ("Item #" .. mat.id)
    frame._matName = matName

    frame.matNameText:ClearAllPoints()
    frame.matNameText:SetPoint("LEFT", 4, 0)
    frame.matNameText:SetWidth(data.mode == "byrecipe" and 130 or 150)
    ApplyFont(frame.matNameText, scheme, "small")
    frame.matNameText:SetText(matName)
    frame.matNameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a or 1)
    frame.matNameText:Show()

    local countStr
    if data.mode == "byrecipe" then
        if mat.have >= mat.needed then
            countStr = HDG.Theme.C_Success() .. mat.needed .. "/" .. mat.needed .. "|r"
        elseif mat.have > 0 then
            countStr = HDG.Theme.C_Warning() .. mat.have .. "/" .. mat.needed .. "|r"
        else
            countStr = HDG.Theme.C_Error() .. "0/" .. mat.needed .. "|r"
        end
        frame.matCountText:ClearAllPoints()
        frame.matCountText:SetPoint("RIGHT", -6, 0)
    else
        if mat.have > mat.needed then
            local excess = mat.have - mat.needed
            countStr = string.format("%s/%s+" .. HDG.Theme.C_Success() .. "%s|r",
                BreakUpLargeNumbers(mat.needed), BreakUpLargeNumbers(mat.needed), BreakUpLargeNumbers(excess))
        elseif mat.have == mat.needed then
            countStr = HDG.Theme.C_Success() .. string.format("%s/%s", BreakUpLargeNumbers(mat.have), BreakUpLargeNumbers(mat.needed)) .. "|r"
        elseif mat.have > 0 then
            countStr = HDG.Theme.C_Warning() .. string.format("%s/%s", BreakUpLargeNumbers(mat.have), BreakUpLargeNumbers(mat.needed)) .. "|r"
        else
            countStr = HDG.Theme.C_Error() .. string.format("0/%s", BreakUpLargeNumbers(mat.needed)) .. "|r"
        end
        frame.matCountText:ClearAllPoints()
        frame.matCountText:SetPoint("RIGHT", -10, 0)
    end
    ApplyFont(frame.matCountText, scheme, "small")
    frame.matCountText:SetText(countStr)
    frame.matCountText:Show()
end

local function BindMatIntermediateKind(frame, data, scheme)
    local inter = data.inter
    frame._kind = "matIntermediate"
    frame._matInter = inter
    frame._matInterRecipe = data.interRecipe

    local bg = { scheme.panel.r, scheme.panel.g * 1.2, scheme.panel.b * 1.2, 0.6 }
    frame._matBgColor = bg
    frame._matHoverBgColor = { 0.15, 0.3, 0.3, 0.8 }
    frame.bg:SetColorTexture(bg[1], bg[2], bg[3], bg[4])

    frame.matProfIcon:ClearAllPoints()
    frame.matProfIcon:SetPoint("LEFT", 8, 0)
    HDG_ExpansionData.ApplyProfessionIcon(frame.matProfIcon, data.interProfession)
    frame.matProfIcon:Show()

    local interName = inter.name or ("Item #" .. inter.id)
    local isKnown = data.isKnown
    frame.matNameText:ClearAllPoints()
    frame.matNameText:SetPoint("LEFT", 8 + 14 + 4, 0)
    frame.matNameText:SetWidth(145)
    ApplyFont(frame.matNameText, scheme, "small")
    if isKnown then
        frame.matNameText:SetText(interName)
        frame.matNameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a or 1)
    else
        frame.matNameText:SetText(interName .. " (unknown)")
        frame.matNameText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a or 1)
    end
    frame.matNameText:Show()

    frame.matCountText:ClearAllPoints()
    frame.matCountText:SetPoint("RIGHT", -10, 0)
    ApplyFont(frame.matCountText, scheme, "small")
    frame.matCountText:SetText(string.format("x%d", inter.needed))
    frame.matCountText:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, scheme.accent.a or 1)
    frame.matCountText:Show()
end

local function BindMatChildRowKind(frame, data, scheme)
    frame._kind = "matChildRow"
    frame._matChildTooltipID = data.tooltipItemID
    frame._matChildName = data.name

    local bg = { 0.08, 0.12, 0.12, 0.4 }
    frame._matBgColor = bg
    frame._matHoverBgColor = { 0.12, 0.18, 0.18, 0.6 }
    frame.bg:SetColorTexture(bg[1], bg[2], bg[3], bg[4])

    frame.matNameText:ClearAllPoints()
    frame.matNameText:SetPoint("LEFT", 20, 0)
    frame.matNameText:SetWidth(140)
    ApplyFont(frame.matNameText, scheme, "small")
    frame.matNameText:SetText("- " .. (data.name or "Unknown"))
    frame.matNameText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a or 1)
    frame.matNameText:Show()

    frame.matCountText:ClearAllPoints()
    frame.matCountText:SetPoint("RIGHT", -8, 0)
    ApplyFont(frame.matCountText, scheme, "small")
    frame.matCountText:SetText("x" .. (data.totalNeeded or 0))
    frame.matCountText:SetTextColor(scheme.text_dim.r * 0.8, scheme.text_dim.g * 0.8, scheme.text_dim.b * 0.8, scheme.text_dim.a or 1)
    frame.matCountText:Show()
end

local function BindSpacerKind(frame)
    frame._kind = "spacer"
    frame._matBgColor = { 0, 0, 0, 0 }
    frame.bg:SetColorTexture(0, 0, 0, 0)
end

local function BindEmptyStateKind(frame, data, scheme)
    frame._kind = "emptyState"
    frame._matBgColor = { 0, 0, 0, 0 }
    frame.bg:SetColorTexture(0, 0, 0, 0)
    ApplyFont(frame.matEmptyText, scheme, "body")
    frame.matEmptyText:SetText(HDG.Theme.C_Dim() .. (data.text or L["RECIPE_EMPTY_QUEUE_HINT"] or "Queue is empty") .. "|r")
    frame.matEmptyText:Show()
end

local function BindMaterialsFrame(frame, data)
    HideAllMaterialsWidgets(frame)
    local scheme = GetScheme()
    local kind = data.kind
    if kind == "matHeader" then
        BindMatHeaderKind(frame, data, scheme)
    elseif kind == "matSubHeader" then
        BindMatSubHeaderKind(frame, data, scheme)
    elseif kind == "matRow" then
        BindMatRowKind(frame, data, scheme)
    elseif kind == "matIntermediate" then
        BindMatIntermediateKind(frame, data, scheme)
    elseif kind == "matChildRow" then
        BindMatChildRowKind(frame, data, scheme)
    elseif kind == "emptyState" then
        BindEmptyStateKind(frame, data, scheme)
    else
        BindSpacerKind(frame)
    end
end

-- ============================================================================
-- DataProvider push helpers
-- ============================================================================

local function PushMatCategory(dp, catKey, category, collapsedMaterials, scheme)
    local isCollapsed = collapsedMaterials[catKey] or false
    dp:Insert({
        kind = "matHeader",
        collapseKey = catKey,
        isCollapsed = isCollapsed,
        icon = category.icon,
        text = category.name,
        textColor = scheme.text_header,
        bgColor = { scheme.panel.r, scheme.panel.g, scheme.panel.b, 1.0 },
        hoverBgColor = { scheme.button_hover.r, scheme.button_hover.g, scheme.button_hover.b, 1.0 },
    })
    if isCollapsed then return end

    for _, mat in ipairs(category.items) do
        -- Precompute per-row price for cost rollup; stored on mat so the cost
        -- calculation outside the loop can read it (matches pre-migration behavior).
        local matPrice = Data().GetItemPrice(mat.id)
        if matPrice then
            mat.estimatedCost = matPrice * math.max(0, mat.needed - mat.have)
        end
        dp:Insert({ kind = "matRow", mat = mat, mode = "totals" })
    end
    dp:Insert({ kind = "spacer", height = 6 })
end

local function PushIntermediatesSection(dp, intermediates, collapsedMaterials, scheme)
    local interArray = {}
    for _, inter in pairs(intermediates) do
        if inter.needed > 0 then table.insert(interArray, inter) end
    end
    if #interArray == 0 then return end
    table.sort(interArray, function(a, b) return (a.name or "") < (b.name or "") end)

    local isCollapsed = collapsedMaterials["intermediates"] or false
    local bg
    if scheme then
        bg = { scheme.button_active.r * 0.5, scheme.button_active.g * 0.6, scheme.button_active.b * 0.6, 0.9 }
    else
        bg = { 0.15, 0.25, 0.25, 0.9 }
    end
    local hover
    if scheme then
        hover = { scheme.button_hover.r, scheme.button_hover.g, scheme.button_hover.b, 0.9 }
    else
        hover = { 0.2, 0.35, 0.35, 0.9 }
    end

    dp:Insert({
        kind = "matHeader",
        collapseKey = "intermediates",
        isCollapsed = isCollapsed,
        icon = "Interface\\Icons\\Trade_Engineering",
        text = HDG.Theme.C_Accent() .. "Craft These First|r",
        bgColor = bg,
        hoverBgColor = hover,
        showIntermediatesTooltip = true,
    })
    if isCollapsed then return end

    for _, inter in ipairs(interArray) do
        local interRecipe = GetRecipeByItemID(inter.id)
        local interProfession = interRecipe and interRecipe.profession or nil
        local isKnown = HDG_PowerCrafting and HDG_PowerCrafting.CanPlayerCraft(inter.id) or false
        dp:Insert({
            kind = "matIntermediate",
            inter = inter,
            interRecipe = interRecipe,
            interProfession = interProfession,
            isKnown = isKnown,
        })
        if inter.children and next(inter.children) then
            for childID, childData in pairs(inter.children) do
                if childData.source ~= "Ignore" then
                    local totalNeeded = (tonumber(childData.perCraft) or 1) * (tonumber(inter.needed) or 1)
                    dp:Insert({
                        kind = "matChildRow",
                        name = childData.name or ("Item #" .. childID),
                        totalNeeded = totalNeeded,
                        tooltipItemID = childData.id,
                    })
                end
            end
        end
    end
    dp:Insert({ kind = "spacer", height = 6 })
end

local function PushMaterialsByRecipe(dp, queue, scheme)
    local recipes
    if HDG.Store:GetConfig("showRawMaterials") and HDG_PowerCrafting then
        recipes = HDG_PowerCrafting.AggregateMaterialsByRecipeRaw(queue)
    else
        recipes = HDG_PowerCrafting.AggregateMaterialsByRecipe(queue)
    end
    local collapsedMaterials = HDG.Store:GetUIState("collapsedMaterials") or {}
    local categoryOrder = { "vendor", "gather", "craft", "unknown" }
    local anyContent = false

    for _, recipeEntry in ipairs(recipes) do
        anyContent = true
        local recipeName = recipeEntry.recipeName
        local recipeQty = recipeEntry.quantity
        local recipeKey = "recipe_" .. (recipeEntry.recipeID or recipeName)
        local isRecipeCollapsed = collapsedMaterials[recipeKey] or false

        local accentBg = { scheme.accent.r * 0.3, scheme.accent.g * 0.3, scheme.accent.b * 0.3, 0.9 }
        local accentHover = { scheme.accent.r * 0.5, scheme.accent.g * 0.5, scheme.accent.b * 0.5, 1.0 }

        dp:Insert({
            kind = "matHeader",
            isRecipeHeader = true,
            collapseKey = recipeKey,
            isCollapsed = isRecipeCollapsed,
            text = HDG.Theme.C_Text() .. recipeName .. "|r",
            qtyText = HDG.Theme.C_Warning() .. "x" .. recipeQty .. "|r",
            bgColor = accentBg,
            hoverBgColor = accentHover,
            textWidth = 170,
        })

        if not isRecipeCollapsed then
            local categorized = HDG_PowerCrafting.CategorizeMaterialsForRecipe(recipeEntry.materials)
            for _, catKey in ipairs(categoryOrder) do
                local category = categorized[catKey]
                if category and #category.items > 0 then
                    dp:Insert({ kind = "matSubHeader", icon = category.icon, text = category.name })
                    for _, mat in ipairs(category.items) do
                        dp:Insert({
                            kind = "matRow",
                            mat = mat,
                            mode = "byrecipe",
                            recipeName = recipeName,
                            recipeQty = recipeQty,
                        })
                    end
                    dp:Insert({ kind = "spacer", height = 4 })
                end
            end
            dp:Insert({ kind = "spacer", height = 4 })
        end
    end

    return anyContent, recipes
end

-- ============================================================================
-- Public API
-- ============================================================================

function HDG_MaterialsPanel.Init(frame)
    mainFrame = frame
end

function HDG_MaterialsPanel.Create()
    local scheme = GetScheme()

    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    local materialsFrame = layout.frame
    materialsFrame:ClearAllPoints()
    materialsFrame:SetPoint("TOPLEFT", mainFrame.queueFrame, "TOPRIGHT", CONFIG.SPACING.PANEL_GAP, 0)
    materialsFrame:SetSize(CONFIG.RIGHT_PANEL_WIDTH, CONFIG.PANEL_HEIGHT)
    mainFrame.materialsFrame = materialsFrame

    local requiredMatsLabel = L["HEADER_REQUIRED_MATERIALS"] or "Required Materials"
    local rawMatsLabel = L["HEADER_RAW_MATERIALS"] or "Raw Materials"
    local directLabel = L["BTN_DIRECT"] or "Direct"
    local rawLabel = L["BTN_RAW"] or "Raw"

    local materialsHeaderBg, materialsTitle = HDG_UIHelpers.CreatePanelHeader(materialsFrame, requiredMatsLabel, { rightPad = -170, justify = "LEFT" })
    mainFrame.materialsHeaderBg = materialsHeaderBg
    mainFrame.materialsTitle = materialsTitle

    -- Sort Mode toggle (Totals | By Recipe). Single FilterButton whose label
    -- and active state flip together each click.
    local function ShowSortModeTooltip(btn)
        local currentMode = HDG.Store:GetConfig("materialSortMode") or "totals"
        GameTooltip:SetOwner(btn, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:ClearLines()
        GameTooltip:AddLine(HDG.Theme.C_Text() .. L["RECIPE_TIP_SORT_MODE"] .. "|r", 1, 1, 1)
        if currentMode == "totals" then
            GameTooltip:AddLine(L["RECIPE_TIP_SORT_TOTALS_DESC"], 0.7, 0.7, 0.7)
            GameTooltip:AddLine(L["RECIPE_TIP_SORT_SWITCH_RECIPE"], 0.5, 0.8, 0.5)
        else
            GameTooltip:AddLine(L["RECIPE_TIP_SORT_RECIPE_DESC"], 0.7, 0.7, 0.7)
            GameTooltip:AddLine(L["RECIPE_TIP_SORT_SWITCH_TOTALS"], 0.5, 0.8, 0.5)
        end
        GameTooltip:Show()
    end

    local function sortModeIsActive()
        return (HDG.Store:GetConfig("materialSortMode") or "totals") == "byRecipe"
    end
    local function sortModeLabel(active)
        return active and L["RECIPE_SORT_BY_RECIPE"] or L["RECIPE_SORT_TOTALS"]
    end

    local sortModeBtn
    sortModeBtn = HDG_UIHelpers.CreateFilterButton(
        materialsHeaderBg, sortModeLabel(sortModeIsActive()), 65, sortModeIsActive(),
        function(self)
            local newMode = sortModeIsActive() and "totals" or "byRecipe"
            HDG.Store:Dispatch("SET_CONFIG", { key = "materialSortMode", value = newMode })
            HDG_UIHelpers.UpdateFilterButtonState(sortModeBtn, sortModeIsActive(), sortModeLabel(sortModeIsActive()))
            HDG_MaterialsPanel.Refresh()
            if GameTooltip:IsOwned(self) then ShowSortModeTooltip(self) end
        end,
        { height = 18 }
    )
    sortModeBtn:SetPoint("RIGHT", -96, 0)
    sortModeBtn:HookScript("OnEnter", function(self) ShowSortModeTooltip(self) end)
    sortModeBtn:HookScript("OnLeave", function() GameTooltip:Hide() end)
    mainFrame.sortModeBtn = sortModeBtn
    mainFrame.sortModeUpdate = function()
        HDG_UIHelpers.UpdateFilterButtonState(sortModeBtn, sortModeIsActive(), sortModeLabel(sortModeIsActive()))
    end

    -- Power Crafting toggle (Direct | Raw). Two paired FilterButtons inside a
    -- shared container; the active one matches the showRawMaterials config.
    local toggleContainer = CreateFrame("Frame", nil, materialsHeaderBg, "BackdropTemplate")
    toggleContainer:SetSize(84, 18)
    toggleContainer:SetPoint("RIGHT", -6, 0)
    toggleContainer:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
        tile = false
    })
    toggleContainer:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.8)
    toggleContainer:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    RegisterWidget(toggleContainer, "Panel")

    local function rawIsActive() return HDG.Store:GetConfig("showRawMaterials") and true or false end

    local directBtn, rawBtn
    local function UpdateToggleAppearance()
        local showRaw = rawIsActive()
        HDG_UIHelpers.UpdateFilterButtonState(directBtn, not showRaw)
        HDG_UIHelpers.UpdateFilterButtonState(rawBtn, showRaw)
        materialsTitle:SetText(showRaw and rawMatsLabel or requiredMatsLabel)
    end

    local function selectMode(showRaw)
        HDG.Store:Dispatch("SET_CONFIG", { key = "showRawMaterials", value = showRaw })
        UpdateToggleAppearance()
        HDG_MaterialsPanel.Refresh()
        if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
            HDG_LumberTracker.RefreshPinned()
        end
    end

    directBtn = HDG_UIHelpers.CreateFilterButton(toggleContainer, directLabel, 40, not rawIsActive(),
        function() selectMode(false) end, { height = 16 })
    directBtn:SetPoint("LEFT", 1, 0)

    rawBtn = HDG_UIHelpers.CreateFilterButton(toggleContainer, rawLabel, 40, rawIsActive(),
        function() selectMode(true) end, { height = 16 })
    rawBtn:SetPoint("LEFT", directBtn, "RIGHT", 1, 0)

    HDG_UIHelpers.SetupTooltip(directBtn, {
        title = HDG.Theme.C_Text() .. L["RECIPE_TIP_DIRECT_MATS"] .. "|r",
        desc  = { L["RECIPE_TIP_DIRECT_MATS_DESC"], L["RECIPE_TIP_DIRECT_AS_IS"] },
    })

    HDG_UIHelpers.SetupTooltip(rawBtn, {
        title = HDG.Theme.C_Text() .. L["RECIPE_TIP_RAW_MATS"] .. "|r",
        desc  = { L["RECIPE_TIP_RAW_MATS_DESC"], L["RECIPE_TIP_RAW_GATHER"] },
    })

    mainFrame.materialsToggleUpdate = UpdateToggleAppearance
    UpdateToggleAppearance()

    materialsHeaderBg:EnableMouse(true)
    HDG_UIHelpers.SetupTooltip(materialsHeaderBg, {
        title = HDG.Theme.C_Text() .. "Required Materials|r",
        desc  = {
            "All materials for your Crafting Queue",
            " ",
            HDG.Theme.C_Warning() .. "Direct vs Raw:|r",
            "Direct: Materials as listed in recipes",
            "Raw: Expand crafted reagents to base materials",
            " ",
            HDG.Theme.C_Warning() .. "How to use:|r",
            "Right-click materials to search Auction House",
            "Use 'Add All to Auctionator' for bulk search",
        },
    })

    -- Materials list scroll (WowScrollBoxList)
    local matScrollBox = CreateFrame("Frame", nil, materialsFrame, "WowScrollBoxList")
    matScrollBox:SetPoint("TOPLEFT", 8, -28)
    matScrollBox:SetPoint("BOTTOMRIGHT", -20, 68)

    local matScrollBar = CreateFrame("EventFrame", nil, materialsFrame, "MinimalScrollBar")
    matScrollBar:SetPoint("TOPLEFT", matScrollBox, "TOPRIGHT", 2, 0)
    matScrollBar:SetPoint("BOTTOMLEFT", matScrollBox, "BOTTOMRIGHT", 2, 0)

    local matView = CreateScrollBoxListLinearView(0, 0, 0, 0, 0)
    matView:SetElementExtentCalculator(GetMaterialsElementExtent)
    matView:SetElementInitializer("Button", function(frame, elementData)
        InitializeMaterialsFrame(frame)
        frame:SetWidth(matScrollBox:GetWidth())
        BindMaterialsFrame(frame, elementData)
    end)

    ScrollUtil.InitScrollBoxListWithScrollBar(matScrollBox, matScrollBar, matView)

    mainFrame.materialsScrollBox = matScrollBox
    mainFrame.materialsScrollBar = matScrollBar
    mainFrame.materialsScrollView = matView

    -- Cost display
    local costText = materialsFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    costText:SetPoint("BOTTOM", 0, 10)
    ApplyFont(costText, scheme, "body")
    costText:SetText(HDG.Theme.C_Dim() .. "No price data|r")
    mainFrame.materialsCostText = costText

    -- Add All to Auctionator button
    local addAllBar = CreateFrame("StatusBar", nil, materialsFrame, "BackdropTemplate")
    addAllBar:SetSize(280, 25)
    addAllBar:SetPoint("BOTTOM", costText, "TOP", 0, 5)
    addAllBar:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
    addAllBar:SetStatusBarColor(unpack(HDG_UIHelpers.STATE_ACTIVE))
    addAllBar:SetMinMaxValues(0, 1)
    addAllBar:SetValue(1)
    HDG_UIHelpers.ApplyBackdrop(addAllBar, "solid",
        {scheme.button_active.r * 0.3, scheme.button_active.g * 0.3, scheme.button_active.b * 0.3, 0.3})

    local addAllLabel = L["BTN_ADD_ALL_AUCTIONATOR"] or "Add All to Auctionator"
    local buttonText = addAllBar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    buttonText:SetPoint("CENTER", 0, 0)
    ApplyFont(buttonText, scheme, "body")
    buttonText:SetText(addAllLabel)
    buttonText:SetShadowOffset(0, 0)
    if scheme then
        buttonText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    end
    mainFrame.addAllAuctionatorText = buttonText

    local clickButton = CreateFrame("Button", nil, addAllBar)
    clickButton:SetAllPoints(addAllBar)
    clickButton:RegisterForClicks("LeftButtonUp")
    clickButton:SetScript("OnClick", function()
        HDG_MaterialsPanel.AddAllToAuctionator()
    end)
    clickButton:SetScript("OnEnter", function(self)
        addAllBar:SetStatusBarColor(0.15, 0.40, 0.40, 1.0)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetText(HDG.Theme.C_Warning() .. addAllLabel .. "|r", 1, 1, 1)
        GameTooltip:AddLine("Creates/replaces Auctionator 'Housing Decor' list", 1, 1, 1)
        GameTooltip:AddLine("with materials needed (minus what you own)", 1, 1, 1)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Click|r to add all materials at once", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Note: This will replace the existing list|r", 0.78, 0.78, 0.78)
        GameTooltip:Show()
    end)
    clickButton:SetScript("OnLeave", function()
        addAllBar:SetStatusBarColor(unpack(HDG_UIHelpers.STATE_ACTIVE))
        GameTooltip:Hide()
    end)

    mainFrame.addAllAuctionatorButton = addAllBar
    if not HDG_PriceIntegration.IsAuctionatorAvailable() then
        addAllBar:Hide()
    end
    return materialsFrame
end

function HDG_MaterialsPanel.Refresh()
    if not mainFrame or not mainFrame.materialsScrollBox then return end

    if mainFrame.addAllAuctionatorText then
        if HDG.Store:GetConfig("showRawMaterials") and HDG_PowerCrafting then
            mainFrame.addAllAuctionatorText:SetText(L["BTN_ADD_ALL_AUCTIONATOR_RAW"] or "Add All to Auctionator (Raw Mats)")
        else
            mainFrame.addAllAuctionatorText:SetText(L["BTN_ADD_ALL_AUCTIONATOR"] or "Add All to Auctionator")
        end
    end

    local scheme = GetScheme()
    local queue = HDG.Store:GetCraftingQueue()
    local sortMode = HDG.Store:GetConfig("materialSortMode") or "totals"
    local dp = CreateDataProvider()
    local hasContent = false

    if sortMode == "byRecipe" then
        local anyContent, recipes = PushMaterialsByRecipe(dp, queue, scheme)
        hasContent = anyContent
        if hasContent then
            local totalCost, missingPrices = 0, 0
            if Data().HasPriceAddon() then
                for _, recipeEntry in ipairs(recipes) do
                    for _, mat in pairs(recipeEntry.materials) do
                        local matPrice = Data().GetItemPrice(mat.id)
                        if matPrice then
                            totalCost = totalCost + (matPrice * math.max(0, mat.needed - mat.have))
                        elseif mat.have < mat.needed then
                            missingPrices = missingPrices + 1
                        end
                    end
                end
            end
            UpdateCostDisplay(totalCost, missingPrices)
        end
    else
        local materials, intermediates
        if HDG.Store:GetConfig("showRawMaterials") and HDG_PowerCrafting then
            materials, intermediates = HDG_PowerCrafting.AggregateRawMaterials(queue)
        else
            materials = HDG_PowerCrafting.AggregateMaterials(queue)
            intermediates = nil
        end
        local sortedMaterials = HDG_PowerCrafting.FilterAndSortMaterials(materials)
        local categorized = HDG_PowerCrafting.CategorizeMaterials(sortedMaterials)

        local collapsedMaterials = HDG.Store:GetUIState("collapsedMaterials") or {}
        local categoryOrder = { "vendor", "gather", "craft", "unknown" }
        for _, catKey in ipairs(categoryOrder) do
            local category = categorized[catKey]
            if category and #category.items > 0 then
                PushMatCategory(dp, catKey, category, collapsedMaterials, scheme)
                hasContent = true
            end
        end
        if intermediates and HDG.Store:GetConfig("showRawMaterials") then
            for _, inter in pairs(intermediates) do
                if inter.needed > 0 then
                    PushIntermediatesSection(dp, intermediates, collapsedMaterials, scheme)
                    hasContent = true
                    break
                end
            end
        end

        if hasContent then
            local totalCost, missingPrices = 0, 0
            if Data().HasPriceAddon() then
                for _, catKey in ipairs(categoryOrder) do
                    local category = categorized[catKey]
                    if category and #category.items > 0 then
                        for _, mat in ipairs(category.items) do
                            if mat.estimatedCost then
                                totalCost = totalCost + mat.estimatedCost
                            elseif mat.have < mat.needed then
                                missingPrices = missingPrices + 1
                            end
                        end
                    end
                end
            end
            UpdateCostDisplay(totalCost, missingPrices)
        end
    end

    if not hasContent then
        dp:Insert({ kind = "emptyState", text = L["RECIPE_EMPTY_QUEUE_HINT"] })
        if mainFrame.materialsCostText then
            mainFrame.materialsCostText:SetText("")
        end
    end

    mainFrame.materialsScrollBox:SetDataProvider(dp, ScrollBoxConstants.RetainScrollPosition)
end

function HDG_MaterialsPanel.AddAllToAuctionator()
    if not C_AddOns.IsAddOnLoaded("Auctionator") or not Auctionator or not Auctionator.API or not Auctionator.API.v1 then
        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Please install Auctionator for search list integration")
        return
    end

    local materials
    local modeLabel
    local queue = HDG.Store:GetCraftingQueue()

    if HDG.Store:GetConfig("showRawMaterials") and HDG_PowerCrafting then
        materials = HDG_PowerCrafting.AggregateRawMaterials(queue)
        modeLabel = "raw"
    else
        materials = HDG_PowerCrafting.AggregateMaterials(queue)
        modeLabel = "direct"
    end

    if not materials or next(materials) == nil then
        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r " .. L["STATUS_NO_MATERIALS_QUEUED"])
        return
    end

    local callerID = "HousingDecorGuide"
    local listName = "Housing Decor"
    local hasQuantitySupport = Auctionator.API.v1.ConvertToSearchString ~= nil

    local sortedMaterials = HDG_PowerCrafting.FilterAndSortMaterials(materials)
    local categorized = HDG_PowerCrafting.CategorizeMaterials(sortedMaterials)
    local searchStrings = {}
    local addedCount = 0

    -- In Raw mode skip "craft" (those are intermediates the player will craft)
    local categoryOrder
    if modeLabel == "raw" then
        categoryOrder = { "vendor", "gather", "unknown" }
    else
        categoryOrder = { "vendor", "gather", "craft", "unknown" }
    end

    local skippedCount = 0
    for _, catKey in ipairs(categoryOrder) do
        local category = categorized[catKey]
        if category and #category.items > 0 then
            for _, mat in ipairs(category.items) do
                local itemName = Data().GetCachedItemName(mat.id)
                if itemName then
                    local totalNeeded = mat.needed or 1
                    local playerHas = Data().GetItemCountWithVariants(mat.id) or 0
                    local stillNeeded = math.max(0, totalNeeded - playerHas)

                    if stillNeeded > 0 then
                        local searchString
                        if hasQuantitySupport then
                            local term = {
                                searchString = itemName,
                                isExact = true,
                                quantity = stillNeeded,
                            }
                            searchString = Auctionator.API.v1.ConvertToSearchString(callerID, term)
                        else
                            searchString = itemName
                        end
                        table.insert(searchStrings, searchString)
                        addedCount = addedCount + 1
                    else
                        skippedCount = skippedCount + 1
                    end
                end
            end
        end
    end

    if Auctionator.API.v1.CreateShoppingList and addedCount > 0 then
        Auctionator.API.v1.CreateShoppingList(callerID, listName, searchStrings)
        local modeText = modeLabel == "raw" and " (raw materials)" or ""
        local qtyText = hasQuantitySupport and " (adjusted for inventory)" or ""
        local skippedText = skippedCount > 0 and string.format(", %d already owned", skippedCount) or ""
        print(string.format(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Added %d material(s)%s to Auctionator search list: %s%s%s",
            addedCount, qtyText, listName, modeText, skippedText))
    elseif addedCount == 0 then
        if skippedCount > 0 then
            print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r You already have all the materials needed!")
        else
            print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r No materials to add to search list")
        end
    end
end
