local _, privateDB = ...

-- ============================================================================
-- RECIPES TAB MODULE
-- Manages all 3 panels of the Recipes tab:
--   Left: Recipe/Profession list
--   Middle: Crafting Queue (Shopping)
--   Right: Required Materials
-- Lumber Tracker is in separate module (HDG_LumberTracker.lua) for widget mode
-- Extracted from HDG_UI.lua in v2.0.3 for maintainability
-- ============================================================================

local CONFIG = privateDB.CONFIG
local L = HDG_L or {} -- Localization
local HDG_RecipesTab = {}
privateDB.HDG_RecipesTab = HDG_RecipesTab

local mainFrame = nil

-- ============================================================================
-- 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


-- Store access helpers for SSoT pattern
local function GetStore()
    return HDG and HDG.Store
end

local function GetState()
    local store = GetStore()
    return store and store:GetState()
end

local function Dispatch(action, payload)
    local store = GetStore()
    if store then
        store:Dispatch(action, payload)
    end
end

-- Convenience accessors
local function GetConfig(key)
    local state = GetState()
    return state and state.config and state.config[key]
end

local function GetUIState(key)
    local state = GetState()
    return state and state.ui and state.ui[key]
end

local function GetShoppingList()
    local state = GetState()
    return state and state.shoppingList or {}
end

local function IsPSLAvailable()
    return C_AddOns and C_AddOns.IsAddOnLoaded("ProfessionShoppingList")
        and SlashCmdList["PROFESSIONSHOPPINGLIST"]
end

-- Row pools for shopping list + materials (avoids frame churn on bag events)
local shoppingHeaderPool, activeShoppingHeaders = {}, {}
local shoppingRowPool, activeShoppingRows = {}, {}
local shoppingSubPool, activeShoppingSubs = {}, {}
local matHeaderPool, activeMatHeaders = {}, {}
local matRowPool, activeMatRows = {}, {}
local matSubPool, activeMatSubs = {}, {}

-- Recipe row pool (created lazily after mainFrame exists)
local recipeRowPool = nil

-- Per-refresh CanCraftRecipe cache (cleared at start of each refresh cycle)
local craftabilityCache = {}
local function GetCachedCraftability(recipe)
    local key = recipe.spell or recipe.item or recipe.id
    if not craftabilityCache[key] then
        craftabilityCache[key] = { HDG_Data.CanCraftRecipe(recipe) }
    end
    return unpack(craftabilityCache[key])
end

-- Use centralized Data() accessor from UIHelpers
local function Data()
    return HDG_UIHelpers.Data()
end

-- Get recipe from ProfessionsDB by output itemID (v2 format uses recipeID as key)
local function GetRecipeByItemID(itemID)
    if HDG_PowerCrafting and HDG_PowerCrafting.GetRecipeByItemID then
        return HDG_PowerCrafting.GetRecipeByItemID(itemID)
    end
    return nil
end

-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================


local function GetShoppingItemColor(item)
    if item.expansion then
        local expColor = HDG_ExpansionData.GetColor(item.expansion)
        return { expColor[1] * 0.5, expColor[2] * 0.5, expColor[3] * 0.5, 0.4 }
    end
    return { 0.08, 0.08, 0.08, 0.85 }
end

-- Track which shopping list items are expanded to show subsidiary recipes
-- Key format: "itemID" for top-level, "parentID:itemID" for nested
-- Value is true/false
local expandedShoppingItems = {}

-- Lazy-built reverse index for legacy shopping items missing profession field
local itemToProfession = nil
local function GetProfessionForItem(itemID)
    if not itemID or not HDG_DecorDB then return nil end
    if not itemToProfession then
        itemToProfession = {}
        for _, recipe in pairs(HDG_DecorDB) do
            if recipe.itemID then itemToProfession[recipe.itemID] = recipe.profession end
        end
    end
    return itemToProfession[itemID]
end

-- ============================================================================
-- CRAFT CONFIRMATION DIALOG
-- ============================================================================

local craftConfirmFrame = nil

-- Create the craft confirmation dialog frame (lazy-init, called once)
local function CreateCraftDialog()
    craftConfirmFrame = CreateFrame("Frame", "HDG_CraftConfirmDialog", UIParent, "BackdropTemplate")
    craftConfirmFrame:SetSize(380, 165)
    craftConfirmFrame:SetPoint("CENTER", 0, 100)
    craftConfirmFrame:SetFrameStrata("DIALOG")
    craftConfirmFrame:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 2,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    local cfScheme = GetScheme()
    craftConfirmFrame:SetBackdropColor(cfScheme.bg.r, cfScheme.bg.g, cfScheme.bg.b, 0.95)
    craftConfirmFrame:SetBackdropBorderColor(cfScheme.accent.r, cfScheme.accent.g, cfScheme.accent.b, 1)
    RegisterWidget(craftConfirmFrame, "Frame")
    craftConfirmFrame:EnableMouse(true)
    craftConfirmFrame:SetMovable(true)
    craftConfirmFrame:RegisterForDrag("LeftButton")
    craftConfirmFrame:SetScript("OnDragStart", craftConfirmFrame.StartMoving)
    craftConfirmFrame:SetScript("OnDragStop", craftConfirmFrame.StopMovingOrSizing)

    -- Title
    local title = craftConfirmFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOP", 0, -12)
    ApplyFont(title, cfScheme, "header")
    title:SetText(HDG.Theme.C_Accent() .. "Craft Confirmation|r")
    craftConfirmFrame.title = title

    -- Item text
    local itemText = craftConfirmFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    itemText:SetPoint("TOP", 0, -40)
    itemText:SetWidth(360)
    ApplyFont(itemText, cfScheme, "body")
    craftConfirmFrame.itemText = itemText

    -- Info text
    local infoText = craftConfirmFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    infoText:SetPoint("TOP", 0, -62)
    infoText:SetWidth(360)
    if cfScheme then
        infoText:SetTextColor(cfScheme.text_dim.r, cfScheme.text_dim.g, cfScheme.text_dim.b, cfScheme.text_dim.a)
    end
    ApplyFont(infoText, cfScheme, "small")
    craftConfirmFrame.infoText = infoText

    -- API note text
    local apiNote = craftConfirmFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    apiNote:SetPoint("TOP", 0, -80)
    apiNote:SetWidth(360)
    if cfScheme then
        apiNote:SetTextColor(cfScheme.text_dim.r * 0.7, cfScheme.text_dim.g * 0.7, cfScheme.text_dim.b * 0.7, cfScheme.text_dim.a)
    end
    ApplyFont(apiNote, cfScheme, "small")
    apiNote:SetText("(Profession window opened - required by Blizzard API)")
    craftConfirmFrame.apiNote = apiNote

    -- Helper function to create styled button
    local function CreateStyledButton(parent, width, color, hoverColor)
        local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
        btn:SetSize(width, 28)
        btn:SetBackdrop({
            bgFile = CONFIG.TEXTURES.WHITE_BG,
            edgeFile = CONFIG.TEXTURES.WHITE_BG,
            tile = false,
            edgeSize = 1,
        })
        btn:SetBackdropColor(unpack(color))
        btn:SetBackdropBorderColor(color[1] + 0.1, color[2] + 0.1, color[3] + 0.1, 1)

        local btnText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        btnText:SetPoint("CENTER", 0, 0)
        ApplyFont(btnText, GetScheme(), "body")
        btn.text = btnText

        btn:SetScript("OnEnter", function(self)
            self:SetBackdropColor(unpack(hoverColor))
        end)
        btn:SetScript("OnLeave", function(self)
            self:SetBackdropColor(unpack(color))
        end)
        return btn
    end

    -- Layout: [Craft N] [-] [+] [Craft Max] [Cancel]
    -- Craft button (green) - crafts selected quantity
    local craftBtn = CreateStyledButton(craftConfirmFrame, 70, { 0.15, 0.7, 0.15, 1 }, { 0.2, 0.85, 0.2, 1 })
    craftBtn:SetPoint("BOTTOMLEFT", 12, 15)
    craftBtn.text:SetText(HDG.Theme.C_Text() .. "Craft 1|r")
    craftConfirmFrame.craftBtn = craftBtn

    -- Minus button (orange)
    local minusBtn = CreateStyledButton(craftConfirmFrame, 28, { 0.6, 0.35, 0.1, 1 }, { 0.75, 0.45, 0.15, 1 })
    minusBtn:SetPoint("LEFT", craftBtn, "RIGHT", 6, 0)
    minusBtn.text:SetText(HDG.Theme.C_Text() .. "-|r")
    craftConfirmFrame.minusBtn = minusBtn

    -- Plus button (orange)
    local plusBtn = CreateStyledButton(craftConfirmFrame, 28, { 0.6, 0.35, 0.1, 1 }, { 0.75, 0.45, 0.15, 1 })
    plusBtn:SetPoint("LEFT", minusBtn, "RIGHT", 4, 0)
    plusBtn.text:SetText(HDG.Theme.C_Text() .. "+|r")
    craftConfirmFrame.plusBtn = plusBtn

    -- Craft Max button (teal)
    local craftMaxBtn = CreateStyledButton(craftConfirmFrame, 70, { 0.08, 0.45, 0.40, 1 }, { 0.12, 0.55, 0.50, 1 })
    craftMaxBtn:SetPoint("LEFT", plusBtn, "RIGHT", 6, 0)
    craftConfirmFrame.craftMaxBtn = craftMaxBtn

    -- Cancel button (gray)
    local cancelBtn = CreateStyledButton(craftConfirmFrame, 60, { 0.25, 0.25, 0.25, 1 }, { 0.35, 0.35, 0.35, 1 })
    cancelBtn:SetPoint("BOTTOMRIGHT", -12, 15)
    cancelBtn.text:SetText(HDG.Theme.C_Text() .. "Cancel|r")
    cancelBtn:SetScript("OnClick", function()
        craftConfirmFrame:Hide()
    end)

    -- Close on Escape
    craftConfirmFrame:SetScript("OnKeyDown", function(self, key)
        if key == "ESCAPE" then
            self:SetPropagateKeyboardInput(false)
            self:Hide()
        else
            self:SetPropagateKeyboardInput(true)
        end
    end)

    -- Checkbox: Keep profession window open
    local keepOpenCheck = CreateFrame("CheckButton", nil, craftConfirmFrame, "UICheckButtonTemplate")
    keepOpenCheck:SetSize(24, 24)
    keepOpenCheck:SetPoint("TOPLEFT", 12, -95)
    keepOpenCheck:SetChecked(not GetConfig("closeProfWindowAfterCraft"))
    keepOpenCheck:SetScript("OnClick", function(self)
        Dispatch("SET_CONFIG", { key = "closeProfWindowAfterCraft", value = not self:GetChecked() })
    end)
    craftConfirmFrame.keepOpenCheck = keepOpenCheck

    local koScheme = GetScheme()
    local keepOpenLabel = craftConfirmFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    keepOpenLabel:SetPoint("LEFT", keepOpenCheck, "RIGHT", 2, 0)
    keepOpenLabel:SetText("Keep profession window open after craft")
    if koScheme then
        keepOpenLabel:SetTextColor(koScheme.text_dim.r, koScheme.text_dim.g, koScheme.text_dim.b, koScheme.text_dim.a)
    end
    ApplyFont(keepOpenLabel, koScheme, "small")

    craftConfirmFrame:Hide()
end

-- Show craft confirmation dialog
function HDG_RecipesTab.ShowCraftConfirmation(item, shoppingListIndex)
    if not item or not item.spell then return end

    local queuedQty = item.quantity or 1
    local _, maxCraftable = HDG_Data.CanCraftRecipe(item)
    maxCraftable = maxCraftable or 0
    -- Use the lesser of queued and craftable (can't craft more than you have mats for)
    local totalQty = math.min(queuedQty, maxCraftable)
    if totalQty <= 0 then totalQty = 1 end -- Fallback to 1 if something went wrong

    -- Create the confirmation frame if it doesn't exist
    if not craftConfirmFrame then
        CreateCraftDialog()
    end

    -- Update checkbox state (in case setting changed elsewhere)
    craftConfirmFrame.keepOpenCheck:SetChecked(not GetConfig("closeProfWindowAfterCraft"))

    -- Track current selected quantity
    craftConfirmFrame.selectedQty = 1
    craftConfirmFrame.maxQty = totalQty

    -- Helper to update craft button text
    local function UpdateCraftBtnText()
        craftConfirmFrame.craftBtn.text:SetText(string.format(HDG.Theme.C_Text() .. "Craft %d|r", craftConfirmFrame.selectedQty))
    end

    -- Update dialog content
    local localizedItemName = Data().GetLocalizedName(item.id, item.name)
    craftConfirmFrame.itemText:SetText(string.format(HDG.Theme.C_Text() .. "%s|r", localizedItemName))
    -- Show +/- and Max buttons if player can craft more than 1 (based on materials)
    if maxCraftable > 1 then
        -- Show queued and craftable amounts
        local infoStr = string.format("Queued: %d | Can craft: " .. HDG.Theme.C_Success() .. "%d|r", queuedQty, maxCraftable)
        craftConfirmFrame.infoText:SetText(infoStr)
        -- Max button shows how many you CAN craft (not limited by queue)
        craftConfirmFrame.craftMaxBtn.text:SetText(string.format(HDG.Theme.C_Text() .. "Max %d|r", maxCraftable))
        craftConfirmFrame.craftMaxBtn:Show()
        craftConfirmFrame.minusBtn:Show()
        craftConfirmFrame.plusBtn:Show()
        -- Update maxQty to be based on craftable, not queued
        craftConfirmFrame.maxQty = maxCraftable
    else
        craftConfirmFrame.infoText:SetText("Craft this item?")
        craftConfirmFrame.craftMaxBtn:Hide()
        craftConfirmFrame.minusBtn:Hide()
        craftConfirmFrame.plusBtn:Hide()
        craftConfirmFrame.maxQty = 1
    end

    -- Store item data
    craftConfirmFrame.currentItem = item
    craftConfirmFrame.currentIndex = shoppingListIndex

    -- Reset quantity and update button text
    craftConfirmFrame.selectedQty = 1
    UpdateCraftBtnText()

    -- Set up Craft button click (crafts selected quantity)
    craftConfirmFrame.craftBtn:SetScript("OnClick", function()
        HDG_RecipesTab.ExecuteCraft(item, shoppingListIndex, craftConfirmFrame.selectedQty)
        craftConfirmFrame:Hide()
    end)

    -- Set up minus button (decrement quantity)
    craftConfirmFrame.minusBtn:SetScript("OnClick", function()
        if craftConfirmFrame.selectedQty > 1 then
            craftConfirmFrame.selectedQty = craftConfirmFrame.selectedQty - 1
            UpdateCraftBtnText()
        end
    end)

    -- Set up plus button (increment quantity)
    craftConfirmFrame.plusBtn:SetScript("OnClick", function()
        if craftConfirmFrame.selectedQty < craftConfirmFrame.maxQty then
            craftConfirmFrame.selectedQty = craftConfirmFrame.selectedQty + 1
            UpdateCraftBtnText()
        end
    end)

    -- Set up Craft Max button click (craft all you can based on materials)
    craftConfirmFrame.craftMaxBtn:SetScript("OnClick", function()
        HDG_RecipesTab.ExecuteCraft(item, shoppingListIndex, craftConfirmFrame.maxQty)
        craftConfirmFrame:Hide()
    end)

    craftConfirmFrame:Show()
end

-- Execute the actual crafting
-- quantity parameter allows crafting 1 or all
-- Note: Profession window should already be open when this is called
function HDG_RecipesTab.ExecuteCraft(item, shoppingListIndex, quantity)
    if not item or not item.spell then
        print(HDG.Theme.C_Error() .. "[HDG]|r Invalid recipe data.")
        return
    end

    -- Default to 1 if not specified
    quantity = quantity or 1
    local recipeID = item.spell

    -- Check if C_TradeSkillUI is available
    if not C_TradeSkillUI then
        print(HDG.Theme.C_Error() .. "[HDG]|r C_TradeSkillUI not available.")
        return
    end

    -- Check if profession window is open (should be, since we open it before showing confirmation)
    local isTradeSkillOpen = C_TradeSkillUI.IsTradeSkillReady and C_TradeSkillUI.IsTradeSkillReady()
    if not isTradeSkillOpen then
        print(HDG.Theme.C_Warning() .. "[HDG]|r Profession window not ready. Please try again.")
        return
    end

    -- Set up pending craft tracking for multi-craft scenarios
    -- This tells the event handler how many crafts to expect before closing window
    if privateDB.pendingCraftInfo then
        privateDB.pendingCraftInfo.itemID = item.id
        privateDB.pendingCraftInfo.remaining = quantity
    end

    -- Craft directly - window is already open
    local success, err = pcall(function()
        C_TradeSkillUI.CraftRecipe(recipeID, quantity, {})
    end)
    if success then
        local localizedCraftName = Data().GetLocalizedName(item.id, item.name)
        print(string.format(HDG.Theme.C_Accent() .. "[HDG]|r Crafting %dx %s...", quantity, localizedCraftName))
    else
        print(string.format(HDG.Theme.C_Error() .. "[HDG]|r CraftRecipe failed: %s", tostring(err)))
        -- Clear pending tracking on failure
        if privateDB.pendingCraftInfo then
            privateDB.pendingCraftInfo.itemID = nil
            privateDB.pendingCraftInfo.remaining = 0
        end
    end
end

-- ============================================================================
-- INITIALIZATION
-- ============================================================================

function HDG_RecipesTab:Init(frame)
    mainFrame = frame
end

-- ============================================================================
-- RECIPE RENDERING (Left Panel)
-- ============================================================================

local function UpdateProfessionSectionHeader(profName)
    if mainFrame.profSectionHeader and mainFrame.profSectionText then
        mainFrame.profSectionText:SetText(profName)
        mainFrame.profSectionHeader:Show()
    end
end

local function HideProfessionSectionHeader()
    if mainFrame.profSectionHeader then
        mainFrame.profSectionHeader:Hide()
    end
end

-- Recipe row pool: reuse Button frames across refreshes instead of creating new ones
local function CreateRecipeRowFactory(parent)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    btn.profIcon = btn:CreateTexture(nil, "ARTWORK")
    btn.profIcon:SetSize(14, 14)
    btn.profIcon:Hide()
    btn.collectedIcon = btn:CreateTexture(nil, "OVERLAY")
    btn.collectedIcon:SetSize(12, 12)
    btn.nameText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    btn.nameText:SetJustifyH("LEFT")
    btn.nameText:SetWordWrap(false)
    btn.qtyText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    btn.qtyText:Hide()
    btn.expText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    return btn
end

local function ResetRecipeRow(btn)
    btn.profIcon:Hide()
    btn.profIcon:ClearAllPoints()
    btn.collectedIcon:ClearAllPoints()
    btn.nameText:ClearAllPoints()
    btn.nameText:SetText("")
    btn.qtyText:ClearAllPoints()
    btn.qtyText:SetText("")
    btn.qtyText:Hide()
    btn.expText:ClearAllPoints()
    btn.expText:SetText("")
    btn:SetScript("OnClick", nil)
    btn:SetScript("OnEnter", nil)
    btn:SetScript("OnLeave", nil)
end

local function EnsureRecipeRowPool()
    if not recipeRowPool then
        recipeRowPool = HDG_UIHelpers.CreateRowPool(CreateRecipeRowFactory)
    end
    return recipeRowPool
end

local function RenderRecipe(recipe, yOffset, showCraftableQty, noProfession, showProfessionIcon)
    local itemName = Data().GetLocalizedName(recipe.id, recipe.name)
    if not itemName or itemName == "Loading..." then itemName = "Item " .. recipe.id end

    local isKnown = Data().PlayerKnowsRecipeBySpellID and Data().PlayerKnowsRecipeBySpellID(recipe.spell) or false
    local isCollected = Data().IsItemCollected(recipe.id)

    -- Check if an alt knows this recipe (only if current char doesn't know it)
    local isAltKnown = false
    local altWhoKnows = nil
    if not isKnown and recipe.spell and HDG_Data.GetCharactersWhoKnowRecipe then
        local altsWhoKnow = HDG_Data.GetCharactersWhoKnowRecipe(recipe.spell, recipe.profession)
        if altsWhoKnow and #altsWhoKnow > 0 then
            for _, altInfo in ipairs(altsWhoKnow) do
                if not altInfo.isCurrent then
                    isAltKnown = true
                    altWhoKnows = altInfo.name
                    break
                end
            end
        end
    end

    local pool = EnsureRecipeRowPool()
    local btn = pool:Acquire(mainFrame.recipeContent)
    btn:SetSize(CONFIG.RECIPE_BUTTON_WIDTH, 18)
    btn:SetPoint("TOPLEFT", 5, yOffset)

    local expColor = HDG_ExpansionData.GetColor(recipe.expansion)
    if expColor then
        btn:SetBackdropColor(expColor[1], expColor[2], expColor[3], 0.25)
    else
        local scheme = GetScheme()
        btn:SetBackdropColor(scheme.button_normal.r, scheme.button_normal.g, scheme.button_normal.b, 0.2)
    end

    -- Calculate left offset based on whether we show profession icon
    local leftOffset = 3

    -- Profession icon (for All Professions mode)
    if showProfessionIcon and recipe._professionName then
        btn.profIcon:ClearAllPoints()
        btn.profIcon:SetPoint("LEFT", leftOffset, 0)
        HDG_ExpansionData.ApplyProfessionIcon(btn.profIcon, recipe._professionName)
        btn.profIcon:Show()
        leftOffset = leftOffset + 16
    end

    btn.collectedIcon:ClearAllPoints()
    btn.collectedIcon:SetPoint("LEFT", leftOffset, 0)
    btn.collectedIcon:SetTexture(isCollected and CONFIG.TEXTURES.CHECKMARK or CONFIG.TEXTURES.XMARK)
    btn.collectedIcon:Show()
    leftOffset = leftOffset + 15

    local btnScheme = GetScheme()
    btn.nameText:ClearAllPoints()
    btn.nameText:SetPoint("LEFT", leftOffset, 0)
    btn.nameText:SetPoint("RIGHT", -55, 0)
    ApplyFont(btn.nameText, btnScheme, "small")

    -- Recipe name coloring from scheme
    local recipeColor
    if noProfession then
        recipeColor = btnScheme.text_recipe_no_prof or {r=0.56, g=0.56, b=0.56, a=1}
    elseif isKnown then
        recipeColor = btnScheme.text_recipe_known or {r=0.80, g=0.29, b=0.09, a=1}
    elseif isAltKnown then
        recipeColor = btnScheme.text_recipe_alt or {r=1, g=1, b=1, a=1}
    else
        recipeColor = btnScheme.text_recipe_unknown or {r=0.69, g=0.69, b=0.69, a=1}
    end
    btn.nameText:SetText(itemName)
    btn.nameText:SetTextColor(recipeColor.r, recipeColor.g, recipeColor.b, recipeColor.a)

    if showCraftableQty then
        btn.qtyText:ClearAllPoints()
        btn.qtyText:SetPoint("RIGHT", -40, 0)
        ApplyFont(btn.qtyText, btnScheme, "small")
        btn.qtyText:Show()

        if recipe._maxCraftable and recipe._maxCraftable > 0 then
            local maxQty = recipe._maxCraftable
            if maxQty > 99 then
                btn.qtyText:SetText(HDG.Theme.C_Success() .. "x99+|r")
            else
                btn.qtyText:SetText(string.format(HDG.Theme.C_Success() .. "x%d|r", maxQty))
            end
        elseif recipe._materialPercent then
            local pct = recipe._materialPercent
            local color = pct >= 75 and HDG.Theme.C_Success() or (pct >= 50 and HDG.Theme.C_Warning() or HDG.Theme.C_Error())
            btn.qtyText:SetText(string.format("%s%.0f%%|r", color, pct))
        else
            btn.qtyText:Hide()
        end
    end

    local expShort = HDG_ExpansionData.GetShortName(recipe.expansion)
    expColor = HDG_ExpansionData.GetColor(recipe.expansion)
    btn.expText:ClearAllPoints()
    btn.expText:SetPoint("RIGHT", -5, 0)
    btn.expText:SetText(expShort)
    btn.expText:SetTextColor(unpack(expColor))
    ApplyFont(btn.expText, btnScheme, "small")

    btn:SetScript("OnClick", function()
        HDG_RecipesTab:AddToShoppingList(recipe)
    end)

    btn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetItemByID(recipe.id)
        GameTooltip:AddLine(" ")

        if isAltKnown and altWhoKnows then
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Known by:|r " .. altWhoKnows, 1, 1, 1)
            GameTooltip:AddLine(" ")
        end

        if recipe.reagents and #recipe.reagents > 0 then
            GameTooltip:AddLine(HDG.Theme.C_Text() .. "Materials:|r")
            for _, reagent in ipairs(recipe.reagents) do
                local have = HDG_Data.GetItemCountWithVariants(reagent.id)
                local need = reagent.amount or 1
                local reagentName = Data().GetCachedItemName(reagent.id) or ("Item " .. reagent.id)
                local color = have >= need and HDG.Theme.C_Success() or (have > 0 and HDG.Theme.C_Warning() or HDG.Theme.C_Error())
                GameTooltip:AddDoubleLine("  " .. reagentName, string.format("%s%d/%d|r", color, have, need), 1, 1, 1, 1,
                    1, 1)
            end
        end

        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Click|r to add to crafting queue", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)

    btn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    return yOffset - 20
end

-- Shared comparator for tier bucketing (used by RenderProfession and RefreshRecipes)
local function sortByPercent(a, b)
    return (a._materialPercent or 0) > (b._materialPercent or 0)
end

-- Render a tier group with header label and recipe rows; returns updated yOffset
local function RenderTierGroup(tier, label, color, yOffset, showProfessionIcon)
    if #tier == 0 then return yOffset end
    local tierHeader = mainFrame.recipeContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    tierHeader:SetPoint("TOPLEFT", 8, yOffset)
    tierHeader:SetText(color .. label .. "|r " .. HDG.Theme.C_Dim() .. "(" .. #tier .. ")|r")
    ApplyFont(tierHeader, GetScheme(), "small")
    yOffset = yOffset - 16
    for _, recipe in ipairs(tier) do
        yOffset = RenderRecipe(recipe, yOffset, true, recipe._noProfession, showProfessionIcon)
    end
    return yOffset - 6
end

-- Shared cost display for materials panel (used by RefreshMaterials and RefreshMaterialsByRecipe)
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() .. "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() .. "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() .. "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() .. "Est. Cost:|r " .. HDG.Theme.C_Success() .. "Have all materials!|r")
    end
end

local function RenderProfession(profData, yOffset, showInlineHeader, skipFixedHeader)
    local internalProfName = profData.name                                          -- English name for lookups
    local profName = HDG_ExpansionData.GetLocalizedProfessionName(internalProfName) -- Localized for display
    local craftableFilterEnabled = HDG_Data.IsCraftableFilterEnabled()

    -- Pre-filter by search and uncollected filters
    local preFilteredItems = {}
    for _, recipe in ipairs(profData.items) do
        if HDG_Data.RecipeMatchesFilters(recipe) then
            table.insert(preFilteredItems, recipe)
        end
    end

    -- Skip this profession entirely if no recipes match filters
    if #preFilteredItems == 0 then
        return yOffset
    end

    local hasProfession = HDG_Data.PlayerHasProfession(internalProfName)

    -- Helper to render inline profession header (used in "All Professions" mode)
    local function renderInlineHeader(headerText)
        if showInlineHeader then
            local ihScheme = GetScheme()
            local inlineHeader = CreateFrame("Frame", nil, mainFrame.recipeContent, "BackdropTemplate")
            inlineHeader:SetSize(CONFIG.RECIPE_BUTTON_WIDTH + 10, 20)
            inlineHeader:SetPoint("TOPLEFT", 0, yOffset)
            inlineHeader:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
            local headerBg = ihScheme.header_bar or ihScheme.panel or {r=0.08, g=0.25, b=0.25, a=0.9}
            inlineHeader:SetBackdropColor(headerBg.r, headerBg.g, headerBg.b, headerBg.a or 0.9)

            -- Show recipe count on right side first (so we know its width)
            local countLabel = inlineHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            countLabel:SetPoint("RIGHT", -8, 0)
            local countText = #preFilteredItems .. " recipes"
            ApplyFont(countLabel, ihScheme, "small")
            countLabel:SetText(HDG.Theme.C_Dim() .. countText .. "|r")

            -- Header label with constrained width to avoid overlap
            local headerLabel = inlineHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            headerLabel:SetPoint("LEFT", 8, 0)
            headerLabel:SetPoint("RIGHT", countLabel, "LEFT", -8, 0)
            headerLabel:SetJustifyH("LEFT")
            headerLabel:SetWordWrap(false)
            ApplyFont(headerLabel, ihScheme, "body")
            headerLabel:SetText(headerText)
            -- Color based on profession known status
            local profColor = hasProfession and (ihScheme.text_recipe_known or {r=0.80, g=0.29, b=0.09, a=1})
                or (ihScheme.text_recipe_no_prof or {r=0.56, g=0.56, b=0.56, a=1})
            headerLabel:SetTextColor(profColor.r, profColor.g, profColor.b, profColor.a)

            yOffset = yOffset - 24
        end
    end

    if craftableFilterEnabled then
        local tier100, tier75to99, tier50to74, tierBelow50 = {}, {}, {}, {}

        for _, recipe in ipairs(preFilteredItems) do
            local isKnown = HDG_Data.PlayerKnowsRecipeBySpellID(recipe.spell)
            local canCraft, maxQty, _, matPercent = GetCachedCraftability(recipe)

            recipe._maxCraftable = maxQty
            recipe._materialPercent = matPercent
            recipe._noProfession = not hasProfession

            if hasProfession and isKnown and canCraft and maxQty > 0 then
                table.insert(tier100, recipe)
            elseif matPercent >= 75 then
                table.insert(tier75to99, recipe)
            elseif matPercent >= 50 then
                table.insert(tier50to74, recipe)
            elseif matPercent > 0 then
                table.insert(tierBelow50, recipe)
            end
        end

        table.sort(tier75to99, sortByPercent)
        table.sort(tier50to74, sortByPercent)
        table.sort(tierBelow50, sortByPercent)

        if #tier100 + #tier75to99 + #tier50to74 + #tierBelow50 == 0 then
            return yOffset
        end

        -- Show inline header for "All Professions" mode
        local notLearnedLabel = L["QUEUE_NEEDS_TRAINING"] or "Not Learned"
        local headerText = profName
        if not hasProfession then
            headerText = profName .. " (" .. notLearnedLabel .. ")"
        end
        renderInlineHeader(headerText)

        -- Update fixed header for single profession mode
        if not skipFixedHeader then
            local fixedHeaderText = profName .. " (" .. (L["HEADER_MATERIALS"] or "Materials") .. ")"
            if not hasProfession then
                fixedHeaderText = profName .. " " .. HDG.Theme.C_Dim() .. "(" .. notLearnedLabel .. ")|r"
            end
            UpdateProfessionSectionHeader(fixedHeaderText)
        end

        yOffset = RenderTierGroup(tier100, "Ready to Craft", HDG.Theme.C_Success(), yOffset)
        local tierLabel = hasProfession and "Almost Ready (75-99%)" or "Have Most Materials (75-99%)"
        yOffset = RenderTierGroup(tier75to99, tierLabel, HDG.Theme.C_Success(), yOffset)
        yOffset = RenderTierGroup(tier50to74, "Halfway There (50-74%)", HDG.Theme.C_Warning(), yOffset)
        yOffset = RenderTierGroup(tierBelow50, "Need Materials (<50%)", HDG.Theme.C_Error(), yOffset)

        return yOffset - CONFIG.SPACING.PROFESSION_GAP
    end

    -- Non-craftable filter mode: show all recipes
    renderInlineHeader(profName)

    if not skipFixedHeader then
        UpdateProfessionSectionHeader(profName)
    end

    for _, recipe in ipairs(preFilteredItems) do
        yOffset = RenderRecipe(recipe, yOffset, false)
    end

    return yOffset - CONFIG.SPACING.PROFESSION_GAP
end

-- ============================================================================
-- CREATE RECIPE PANEL (Left)
-- ============================================================================

-- Shared helper: Create a panel header bar with themed title
local function CreatePanelHeader(parent, title, opts)
    opts = opts or {}
    local scheme = GetScheme()
    local headerBg = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    headerBg:SetPoint("TOPLEFT", 1, -1)
    headerBg:SetPoint("TOPRIGHT", -1, -1)
    headerBg:SetHeight(opts.height or 22)
    headerBg:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    headerBg:SetBackdropColor(scheme.panel_header.r, scheme.panel_header.g, scheme.panel_header.b, scheme.panel_header.a)
    RegisterWidget(headerBg, "PanelHeader")

    local titleFS = headerBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    if opts.center then
        titleFS:SetPoint("CENTER", 0, 0)
    else
        titleFS:SetPoint("LEFT", 8, 0)
        if opts.rightPad then titleFS:SetPoint("RIGHT", opts.rightPad, 0) end
        if opts.justify then titleFS:SetJustifyH(opts.justify) end
    end
    ApplyFont(titleFS, scheme, "body")
    titleFS:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    titleFS:SetText(title)
    HDG.Theme.StyleHeaderTitle(titleFS, headerBg)

    return headerBg, titleFS
end

-- Shared helper: Create a scroll frame with styled scrollbar
local function CreateScrollPane(parent, contentWidth, opts)
    opts = opts or {}
    local scroll = CreateFrame("ScrollFrame", nil, parent, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", opts.left or 8, opts.top or -28)
    scroll:SetPoint("BOTTOMRIGHT", opts.right or -26, opts.bottom or 8)

    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(contentWidth, 1)
    scroll:SetScrollChild(content)

    if HDG_UIHelpers and HDG_UIHelpers.StyleScrollBar then
        HDG_UIHelpers.StyleScrollBar(scroll)
    end

    return scroll, content
end

function HDG_RecipesTab:CreateRecipePanel()
    local scheme = GetScheme()

    -- Use factory for panel backdrop (reposition for 3-column layout)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    local recipeFrame = layout.frame
    recipeFrame:ClearAllPoints()
    recipeFrame:SetPoint("TOPLEFT", CONFIG.SPACING.PANEL_PADDING, -(CONFIG.BANNER_HEIGHT + CONFIG.TAB_HEIGHT + 8))
    recipeFrame:SetSize(CONFIG.LEFT_PANEL_WIDTH, CONFIG.PANEL_HEIGHT)
    mainFrame.recipeFrame = recipeFrame

    -- Header
    local recipeHeaderBg, recipeTitle = CreatePanelHeader(recipeFrame, "Professions")
    mainFrame.recipeHeaderBg = recipeHeaderBg
    mainFrame.recipeTitle = recipeTitle

    -- Column header icons with tooltips for totals
    local COL_WIDTH = 60
    local HEADER_OFFSET = 7
    local COLLECTION_COL_LEFT = -6 - COL_WIDTH + HEADER_OFFSET
    local RECIPES_COL_LEFT = -68 - COL_WIDTH + HEADER_OFFSET

    -- Recipes column header (book icon + percentage)
    local recipesHeaderFrame = CreateFrame("Frame", nil, recipeHeaderBg)
    recipesHeaderFrame:SetSize(50, 20)
    recipesHeaderFrame:SetPoint("RIGHT", RECIPES_COL_LEFT + 50, 0)
    recipesHeaderFrame:EnableMouse(true)
    mainFrame.recipesHeaderFrame = recipesHeaderFrame

    local headerRecipesIcon = recipesHeaderFrame:CreateTexture(nil, "OVERLAY")
    headerRecipesIcon:SetSize(14, 14)
    headerRecipesIcon:SetPoint("LEFT", 0, 0)
    headerRecipesIcon:SetTexture("Interface\\Icons\\INV_Misc_Book_09")
    headerRecipesIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    local headerRecipesPct = recipesHeaderFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    headerRecipesPct:SetPoint("LEFT", headerRecipesIcon, "RIGHT", 2, 0)
    ApplyFont(headerRecipesPct, scheme, "small")
    mainFrame.headerRecipesPct = headerRecipesPct

    -- Collection column header (crate icon + percentage)
    local collectionHeaderFrame = CreateFrame("Frame", nil, recipeHeaderBg)
    collectionHeaderFrame:SetSize(50, 20)
    collectionHeaderFrame:SetPoint("RIGHT", COLLECTION_COL_LEFT + 50, 0)
    collectionHeaderFrame:EnableMouse(true)
    mainFrame.collectionHeaderFrame = collectionHeaderFrame

    local headerCollectionIcon = collectionHeaderFrame:CreateTexture(nil, "OVERLAY")
    headerCollectionIcon:SetSize(14, 14)
    headerCollectionIcon:SetPoint("LEFT", 0, 0)
    headerCollectionIcon:SetTexture("Interface\\Icons\\INV_Crate_01")
    headerCollectionIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    local headerCollectionPct = collectionHeaderFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    headerCollectionPct:SetPoint("LEFT", headerCollectionIcon, "RIGHT", 2, 0)
    ApplyFont(headerCollectionPct, scheme, "small")
    mainFrame.headerCollectionPct = headerCollectionPct

    -- Search box row (right below header)
    local searchRow = CreateFrame("Frame", nil, recipeFrame)
    searchRow:SetPoint("TOPLEFT", 8, -24)
    searchRow:SetPoint("TOPRIGHT", -8, -24)
    searchRow:SetHeight(20)

    -- Profession tabs container (pushed down by search row)
    local professionTabs = CreateFrame("Frame", nil, recipeFrame)
    professionTabs:SetPoint("TOPLEFT", 8, -46)
    professionTabs:SetPoint("TOPRIGHT", -8, -46)
    professionTabs:SetHeight(188)
    mainFrame.professionTabs = professionTabs

    -- Search box
    local searchBox = CreateFrame("EditBox", nil, searchRow, "BackdropTemplate")
    searchBox:SetPoint("LEFT", 0, 0)
    searchBox:SetSize(115, 20)
    HDG_UIHelpers.ApplyBackdrop(searchBox, "panel",
        {scheme.bg.r, scheme.bg.g, scheme.bg.b, 1},
        {scheme.border.r, scheme.border.g, scheme.border.b, 0.8})
    searchBox:SetAutoFocus(false)
    searchBox:SetTextInsets(6, 20, 0, 0)
    searchBox:SetMaxLetters(50)
    ApplyFont(searchBox, scheme, "small")

    -- Search placeholder text
    local searchPlaceholder = searchBox:CreateFontString(nil, "ARTWORK")
    searchPlaceholder:SetPoint("LEFT", 6, 0)
    ApplyFont(searchPlaceholder, scheme, "small")
    searchPlaceholder:SetText("Search recipes...")
    if scheme then
        searchPlaceholder:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, 0.6)
    end
    searchBox.placeholder = searchPlaceholder

    -- Clear button (X) for search
    local searchClear = CreateFrame("Button", nil, searchBox)
    searchClear:SetSize(14, 14)
    searchClear:SetPoint("RIGHT", -3, 0)
    searchClear:Hide()
    local searchClearTex = searchClear:CreateTexture(nil, "ARTWORK")
    searchClearTex:SetAllPoints()
    searchClearTex:SetTexture("Interface\\Buttons\\UI-StopButton")
    searchClear:SetScript("OnClick", function()
        searchBox:SetText("")
        searchBox:ClearFocus()
        HDG_Data.SetSearchFilter("")
        HDG_UI.RefreshUI()
    end)

    searchBox:SetScript("OnTextChanged", function(self, userInput)
        local text = self:GetText()
        if text and text ~= "" then
            searchPlaceholder:Hide()
            searchClear:Show()
        else
            searchPlaceholder:Show()
            searchClear:Hide()
        end
        if userInput then
            HDG_Data.SetSearchFilter(text)
            HDG_UI.RefreshUI()
        end
    end)
    searchBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    searchBox:SetScript("OnEnterPressed", function(self)
        self:ClearFocus()
    end)
    mainFrame.recipeSearchBox = searchBox
    RegisterWidget(searchBox, "SearchBox")

    -- Uncollected checkbox
    local uncollectedCheck = CreateFrame("CheckButton", nil, searchRow, "UICheckButtonTemplate")
    uncollectedCheck:SetPoint("LEFT", searchBox, "RIGHT", 4, 0)
    uncollectedCheck:SetSize(20, 20)
    uncollectedCheck:SetChecked(HDG_Data.IsUncollectedFilterEnabled())

    local uncollectedLabel = searchRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    uncollectedLabel:SetPoint("LEFT", uncollectedCheck, "RIGHT", 0, 0)
    uncollectedLabel:SetText(HDG.Theme.C_Text() .. (L["CHECK_UNCOLLECTED_ONLY"] or "Uncollected") .. "|r")
    ApplyFont(uncollectedLabel, scheme, "small")

    uncollectedCheck:SetScript("OnClick", function(self)
        HDG_Data.SetUncollectedFilter(self:GetChecked())
        HDG_UI.RefreshUI()
    end)
    uncollectedCheck:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Show Uncollected Only|r", 1, 1, 1)
        GameTooltip:AddLine("Hide recipes for decor you've already collected", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    uncollectedCheck:SetScript("OnLeave", function() GameTooltip:Hide() end)
    mainFrame.uncollectedCheck = uncollectedCheck

    -- Search reagents checkbox
    local searchReagentsCheck = CreateFrame("CheckButton", nil, searchRow, "UICheckButtonTemplate")
    searchReagentsCheck:SetPoint("LEFT", uncollectedLabel, "RIGHT", 4, 0)
    searchReagentsCheck:SetSize(20, 20)
    searchReagentsCheck:SetChecked(HDG_Data.IsSearchReagentsEnabled())

    local searchReagentsLabel = searchRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    searchReagentsLabel:SetPoint("LEFT", searchReagentsCheck, "RIGHT", 0, 0)
    searchReagentsLabel:SetText(HDG.Theme.C_Text() .. (L["CHECK_INCLUDE_MATS"] or "+Mats") .. "|r")
    ApplyFont(searchReagentsLabel, scheme, "small")

    searchReagentsCheck:SetScript("OnClick", function(self)
        HDG_Data.SetSearchReagents(self:GetChecked())
        HDG_UI.RefreshUI()
    end)
    searchReagentsCheck:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Search Reagents|r", 1, 1, 1)
        GameTooltip:AddLine("Also search by reagent/material names", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("e.g. search 'bismuth' to find recipes using Bismuth", 0.78, 0.78, 0.78)
        GameTooltip:Show()
    end)
    searchReagentsCheck:SetScript("OnLeave", function() GameTooltip:Hide() end)
    mainFrame.searchReagentsCheck = searchReagentsCheck

    -- Sticky header (offset by search row height)
    local stickyHeader = CreateFrame("Frame", nil, recipeFrame, "BackdropTemplate")
    stickyHeader:SetPoint("TOPLEFT", 8, -258)
    stickyHeader:SetPoint("TOPRIGHT", -26, -258)
    stickyHeader:SetHeight(18)
    HDG_UIHelpers.ApplyBackdrop(stickyHeader, "solid", {scheme.panel.r, scheme.panel.g, scheme.panel.b, 1.0})
    RegisterWidget(stickyHeader, "Panel")
    stickyHeader:Hide()
    mainFrame.stickyHeader = stickyHeader

    local stickyHeaderText = stickyHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    stickyHeaderText:SetPoint("LEFT", 6, 0)
    ApplyFont(stickyHeaderText, scheme, "small")
    stickyHeaderText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    mainFrame.stickyHeaderText = stickyHeaderText
    RegisterWidget(stickyHeaderText, "Text")

    local stickyHeaderPercentage = stickyHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    stickyHeaderPercentage:SetPoint("RIGHT", -6, 0)
    ApplyFont(stickyHeaderPercentage, scheme, "small")
    stickyHeaderPercentage:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    mainFrame.stickyHeaderPercentage = stickyHeaderPercentage
    RegisterWidget(stickyHeaderPercentage, "Text")

    -- Profession section header (offset by search row height)
    local profSectionHeader = CreateFrame("Frame", nil, recipeFrame, "BackdropTemplate")
    profSectionHeader:SetPoint("TOPLEFT", 1, -258)
    profSectionHeader:SetPoint("TOPRIGHT", -1, -258)
    profSectionHeader:SetHeight(22)
    HDG_UIHelpers.ApplyBackdrop(profSectionHeader, "panel",
        {scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.8},
        {scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a})
    profSectionHeader:Hide()
    mainFrame.profSectionHeader = profSectionHeader
    RegisterWidget(profSectionHeader, "Panel")

    local profSectionTextFrame = CreateFrame("Frame", nil, profSectionHeader)
    profSectionTextFrame:SetPoint("LEFT", 8, 0)
    profSectionTextFrame:SetSize(150, 20)

    local profSectionText = profSectionTextFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    profSectionText:SetPoint("LEFT", 0, 0)
    ApplyFont(profSectionText, scheme, "body")
    profSectionText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    mainFrame.profSectionText = profSectionText
    RegisterWidget(profSectionText, "Text")

    profSectionTextFrame:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Profession Progress|r", 1, 1, 1)
        GameTooltip:AddLine("Shows recipes collected / total recipes", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Workflow:|r", 1, 1, 1)
        GameTooltip:AddLine("1. Click recipes to add to Crafting Queue", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("2. Review Required Materials", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("3. Right-click materials for AH search", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    profSectionTextFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- All/Ready toggle
    local btnWidth = 50
    local toggleContainer = CreateFrame("Frame", nil, profSectionHeader)
    toggleContainer:SetPoint("TOPRIGHT", 0, 0)
    toggleContainer:SetPoint("BOTTOMRIGHT", 0, 0)
    toggleContainer:SetWidth(btnWidth * 2)

    local allBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    allBtn:SetPoint("TOPLEFT", 0, 0)
    allBtn:SetPoint("BOTTOMLEFT", 0, 0)
    allBtn:SetWidth(btnWidth)
    allBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    local allBtnText = allBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    allBtnText:SetPoint("CENTER", 0, 0)
    ApplyFont(allBtnText, scheme, "small")
    allBtnText:SetText(L["BTN_ALL"] or "All")

    local allBtnLeftBorder = allBtn:CreateTexture(nil, "OVERLAY")
    allBtnLeftBorder:SetTexture(CONFIG.TEXTURES.WHITE_BG)
    allBtnLeftBorder:SetVertexColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    allBtnLeftBorder:SetPoint("TOPLEFT", 0, 0)
    allBtnLeftBorder:SetPoint("BOTTOMLEFT", 0, 0)
    allBtnLeftBorder:SetWidth(1)
    allBtnLeftBorder:Hide()

    local readyBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    readyBtn:SetPoint("TOPRIGHT", 0, 0)
    readyBtn:SetPoint("BOTTOMRIGHT", 0, 0)
    readyBtn:SetWidth(btnWidth)
    readyBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    local readyBtnText = readyBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    readyBtnText:SetPoint("CENTER", 0, 0)
    ApplyFont(readyBtnText, scheme, "small")
    readyBtnText:SetText(L["BTN_READY"] or "Ready")

    local function UpdateCraftableToggle()
        local s = GetScheme()
        local isReady = HDG_Data.IsCraftableFilterEnabled()
        allBtn:SetBackdropColor(isReady and 0.05 or s.button_active.r, isReady and 0.05 or s.button_active.g, isReady and 0.05 or s.button_active.b, isReady and 0 or 0.9)
        allBtnText:SetTextColor(isReady and s.text_dim.r or s.text.r, isReady and s.text_dim.g or s.text.g, isReady and s.text_dim.b or s.text.b, isReady and s.text_dim.a or s.text.a)
        readyBtn:SetBackdropColor(isReady and s.button_active.r or 0.05, isReady and s.button_active.g or 0.05, isReady and s.button_active.b or 0.05, isReady and 0.9 or 0)
        readyBtnText:SetTextColor(isReady and s.text.r or s.text_dim.r, isReady and s.text.g or s.text_dim.g, isReady and s.text.b or s.text_dim.b, isReady and s.text.a or s.text_dim.a)
        allBtnLeftBorder:SetShown(isReady)
    end

    allBtn:SetScript("OnClick", function()
        if HDG_Data.IsCraftableFilterEnabled() then
            HDG_Data.SetCraftableFilter(false)
            UpdateCraftableToggle()
            HDG_UI.RefreshUI()
        end
    end)

    readyBtn:SetScript("OnClick", function()
        if not HDG_Data.IsCraftableFilterEnabled() then
            HDG_Data.SetCraftableFilter(true)
            UpdateCraftableToggle()
            HDG_UI.RefreshUI()
        end
    end)

    allBtn:SetScript("OnEnter", function(self)
        if HDG_Data.IsCraftableFilterEnabled() then
            local s = GetScheme()
            self:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, 0.9)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Show All Recipes|r", 1, 1, 1)
        GameTooltip:Show()
    end)
    allBtn:SetScript("OnLeave", function() UpdateCraftableToggle(); GameTooltip:Hide() end)

    readyBtn:SetScript("OnEnter", function(self)
        if not HDG_Data.IsCraftableFilterEnabled() then
            local s = GetScheme()
            self:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, 0.9)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Show Ready to Craft|r", 1, 1, 1)
        GameTooltip:AddLine("Filter by material availability", 1, 1, 1)
        GameTooltip:Show()
    end)
    readyBtn:SetScript("OnLeave", function() UpdateCraftableToggle(); GameTooltip:Hide() end)

    mainFrame.UpdateCraftableButton = UpdateCraftableToggle
    UpdateCraftableToggle()

    -- Scroll frame
    local scroll, content = CreateScrollPane(recipeFrame, CONFIG.RECIPE_BUTTON_WIDTH, { top = -284 })
    mainFrame.recipeContent = content
    mainFrame.recipeScroll = scroll

    return recipeFrame
end

-- ============================================================================
-- CREATE SHOPPING PANEL (Middle - Crafting Queue)
-- ============================================================================

function HDG_RecipesTab:CreateShoppingPanel()
    local scheme = GetScheme()

    -- Use factory for panel backdrop (reposition for 3-column layout)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    local shoppingFrame = layout.frame
    shoppingFrame:ClearAllPoints()
    shoppingFrame:SetPoint("TOPLEFT", mainFrame.recipeFrame, "TOPRIGHT", CONFIG.SPACING.PANEL_GAP, 0)
    shoppingFrame:SetSize(CONFIG.MIDDLE_PANEL_WIDTH, CONFIG.PANEL_HEIGHT)
    mainFrame.shoppingFrame = shoppingFrame

    -- Header
    local craftingQueueLabel = L["HEADER_CRAFTING_QUEUE"] or "Crafting Queue"
    local shoppingHeaderBg, shoppingTitle = CreatePanelHeader(shoppingFrame, craftingQueueLabel, { center = true })
    mainFrame.shoppingHeaderBg = shoppingHeaderBg
    mainFrame.shoppingTitle = shoppingTitle
    mainFrame.craftingQueueLabel = craftingQueueLabel

    shoppingHeaderBg:EnableMouse(true)
    shoppingHeaderBg:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "" .. craftingQueueLabel .. "|r", 1, 1, 1)
        GameTooltip:AddLine("Recipes you want to craft", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "How to use:|r", 1, 1, 1)
        GameTooltip:AddLine("Click recipes on the left to add them here", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Click items here to remove them", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    shoppingHeaderBg:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Clear button
    local clearBtn = CreateFrame("Button", nil, shoppingFrame)
    clearBtn:SetSize(20, 20)
    clearBtn:SetPoint("TOPRIGHT", -4, -2)
    clearBtn:SetFrameLevel(shoppingFrame:GetFrameLevel() + 10)
    clearBtn:RegisterForClicks("AnyUp")
    local clearIcon = clearBtn:CreateTexture(nil, "ARTWORK")
    clearIcon:SetAllPoints()
    clearIcon:SetAtlas("common-icon-delete")
    clearBtn:SetScript("OnClick", function()
        Dispatch("CLEAR_SHOPPING_LIST", {})
        HDG_UI.RefreshUI()
    end)
    clearBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "" .. (L["BTN_CLEAR"] or "Clear") .. " " .. craftingQueueLabel .. "|r", 1, 1, 1)
        GameTooltip:AddLine("Remove all items from crafting queue", 1, 1, 1)
        GameTooltip:Show()
    end)
    clearBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Send to PSL button (Profession Shopping List integration)
    local pslBtn = CreateFrame("Button", nil, shoppingFrame)
    pslBtn:SetSize(20, 20)
    pslBtn:SetPoint("TOPRIGHT", -28, -2)
    pslBtn:SetFrameLevel(shoppingFrame:GetFrameLevel() + 10)
    pslBtn:RegisterForClicks("AnyUp")
    local pslIcon = pslBtn:CreateTexture(nil, "ARTWORK")
    pslIcon:SetAllPoints()
    pslIcon:SetAtlas("Professions-Icon-Queue")
    pslBtn:SetScript("OnClick", function()
        local list = GetShoppingList()
        local sent, skipped = 0, 0
        for _, item in ipairs(list) do
            if item.spell then
                SlashCmdList["PROFESSIONSHOPPINGLIST"]("track " .. item.spell .. " " .. (item.quantity or 1))
                sent = sent + 1
            else
                skipped = skipped + 1
            end
        end
        local msg = HDG.Theme.C_Accent() .. "[HDG]|r Sent " .. sent .. " recipe(s) to Profession Shopping List"
        if skipped > 0 then msg = msg .. " (" .. skipped .. " skipped)" end
        print(msg)
    end)
    pslBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Send to PSL|r", 1, 1, 1)
        GameTooltip:AddLine("Export crafting queue to Profession Shopping List", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    pslBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    pslBtn:SetShown(IsPSLAvailable() and #GetShoppingList() > 0)
    mainFrame.pslBtn = pslBtn

    -- Scroll frame - bottom adjusts based on lumber frame visibility
    -- Reserve space for embedded lumber frame: header (24) + rows (lumber count * 18) + padding (8) + margin (8)
    local lumberReservedHeight = 24 + (#HDG_ExpansionData.LUMBER_ORDER * 18) + 8 + 8
    local scroll, content = CreateScrollPane(shoppingFrame, CONFIG.SHOPPING_ITEM_WIDTH, { bottom = lumberReservedHeight })
    mainFrame.shoppingScroll = scroll
    mainFrame.shoppingContent = content

    -- Embedded Lumber Frame (shown when NOT pinned)
    local lumberFrame = CreateFrame("Frame", nil, shoppingFrame, "BackdropTemplate")
    lumberFrame:SetPoint("BOTTOMLEFT", 8, 8)
    lumberFrame:SetPoint("BOTTOMRIGHT", -8, 8)
    -- Dynamic height: header (24) + rows (lumber count * 18) + padding (8)
    local lumberCount = #HDG_ExpansionData.LUMBER_ORDER
    local embeddedLumberHeight = 24 + (lumberCount * 18) + 8
    lumberFrame:SetHeight(embeddedLumberHeight)
    HDG_UIHelpers.ApplyBackdrop(lumberFrame, "panel",
        {scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.8},
        {scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a})
    RegisterWidget(lumberFrame, "Panel")
    mainFrame.lumberFrame = lumberFrame

    -- Pin button in lumber frame header (top-right)
    local pinBtn = CreateFrame("Button", nil, lumberFrame, "BackdropTemplate")
    pinBtn:SetSize(20, 20)
    pinBtn:SetPoint("TOPRIGHT", -4, -4)
    HDG_UIHelpers.ApplyBackdrop(pinBtn, "panel", HDG_UIHelpers.BTN_NORMAL, HDG_UIHelpers.BORDER_COLOR)

    local pinIcon = pinBtn:CreateTexture(nil, "ARTWORK")
    pinIcon:SetSize(14, 14)
    pinIcon:SetPoint("CENTER", 0, 0)
    pinIcon:SetTexture(CONFIG.ADDON_TEXTURES.PIN)
    pinIcon:SetDesaturated(false)
    pinIcon:SetVertexColor(1, 1, 1, 1) -- Full color when unpinned (click to pin)
    pinBtn.icon = pinIcon

    pinBtn:SetScript("OnClick", function()
        Dispatch("SET_LUMBER_PINNED", { pinned = true })
        if HDG_LumberTracker then
            HDG_LumberTracker.ShowPinned()
        end
        if HDG_UI and HDG_UI.UpdateLumberLayout then
            HDG_UI.UpdateLumberLayout()
        end
        -- Update config tab checkbox to reflect change
        if HDG_ConfigTab and HDG_ConfigTab.UpdateLumberPinCheckbox then
            HDG_ConfigTab.UpdateLumberPinCheckbox()
        end
        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Lumber tracker pinned to separate window")
    end)

    pinBtn:SetScript("OnEnter", function(self)
        self:SetBackdropColor(unpack(HDG_UIHelpers.BTN_HOVER))
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Pin Lumber Tracker|r", 1, 1, 1)
        GameTooltip:AddLine("Move to separate floating window", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Stays visible when addon window closes", 0.5, 0.8, 0.5)
        GameTooltip:Show()
    end)

    pinBtn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(unpack(HDG_UIHelpers.BTN_NORMAL))
        GameTooltip:Hide()
    end)

    lumberFrame.pinBtn = pinBtn

    -- Lumber headers (smaller font) - localized
    local lumberHeader = lumberFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    lumberHeader:SetPoint("TOP", 0, -6)
    lumberHeader:SetPoint("LEFT", 4, 0)
    lumberHeader:SetWidth(CONFIG.LUMBER_COL_1)
    lumberHeader:SetJustifyH("LEFT")
    lumberHeader:SetText(HDG.Theme.C_Text() .. (L["COL_TYPE"] or "Type") .. "|r")
    ApplyFont(lumberHeader, scheme, "small")

    local shoppingHeader = lumberFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    shoppingHeader:SetPoint("TOP", 0, -6)
    shoppingHeader:SetPoint("LEFT", 4 + CONFIG.LUMBER_COL_1, 0)
    shoppingHeader:SetWidth(CONFIG.LUMBER_COL_2)
    shoppingHeader:SetJustifyH("CENTER")
    shoppingHeader:SetText(HDG.Theme.C_Text() .. (L["COL_NEEDED"] or "Queue") .. "|r")
    ApplyFont(shoppingHeader, scheme, "small")

    local allHeader = lumberFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    allHeader:SetPoint("TOP", 0, -6)
    allHeader:SetPoint("LEFT", 4 + CONFIG.LUMBER_COL_1 + CONFIG.LUMBER_COL_2, 0)
    allHeader:SetWidth(CONFIG.LUMBER_COL_3)
    allHeader:SetJustifyH("CENTER")
    allHeader:SetText(HDG.Theme.C_Text() .. (L["COL_IN_STOCK"] or "Uncollected") .. "|r")
    ApplyFont(allHeader, scheme, "small")

    local lumberContent = CreateFrame("Frame", nil, lumberFrame)
    lumberContent:SetPoint("TOPLEFT", 4, -24)
    lumberContent:SetPoint("BOTTOMRIGHT", -4, 4)
    mainFrame.lumberContent = lumberContent

    return shoppingFrame
end

-- ============================================================================
-- CREATE MATERIALS PANEL (Right)
-- ============================================================================

function HDG_RecipesTab:CreateMaterialsPanel()
    local scheme = GetScheme()

    -- Use factory for panel backdrop (reposition for 3-column layout)
    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

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

    -- Header
    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 = CreatePanelHeader(materialsFrame, requiredMatsLabel, { rightPad = -170, justify = "LEFT" })
    mainFrame.materialsHeaderBg = materialsHeaderBg
    mainFrame.materialsTitle = materialsTitle

    -- Sort Mode toggle button (Totals | By Recipe)
    local sortModeBtn = CreateFrame("Button", nil, materialsHeaderBg, "BackdropTemplate")
    sortModeBtn:SetSize(65, 18)
    sortModeBtn:SetPoint("RIGHT", -96, 0) -- Position left of toggleContainer
    sortModeBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
        tile = false
    })
    sortModeBtn:SetBackdropColor(scheme.bg.r, scheme.bg.g, scheme.bg.b, 0.8)
    sortModeBtn:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a)
    RegisterWidget(sortModeBtn, "Button")

    local sortModeText = sortModeBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    sortModeText:SetPoint("CENTER", 0, 0)
    ApplyFont(sortModeText, scheme, "small")
    mainFrame.sortModeBtn = sortModeBtn
    mainFrame.sortModeText = sortModeText

    local function UpdateSortModeAppearance()
        local isByRecipe = (GetConfig("materialSortMode") or "totals") == "byRecipe"
        local s = GetScheme()
        local bg = isByRecipe and s.button_hover or s.panel
        local border = isByRecipe and s.accent or s.border
        local text = isByRecipe and s.text or s.text_dim
        sortModeBtn:SetBackdropColor(bg.r, bg.g, bg.b, isByRecipe and bg.a or 0.8)
        sortModeBtn:SetBackdropBorderColor(border.r, border.g, border.b, isByRecipe and 0.8 or border.a)
        sortModeText:SetText(isByRecipe and "By Recipe" or "Totals")
        sortModeText:SetTextColor(text.r, text.g, text.b, text.a)
    end

    local function ShowSortModeTooltip(btn)
        local currentMode = GetConfig("materialSortMode") or "totals"
        GameTooltip:SetOwner(btn, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:ClearLines()
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Material Sort Mode|r", 1, 1, 1)
        if currentMode == "totals" then
            GameTooltip:AddLine("Currently: Totals (aggregated)", 0.7, 0.7, 0.7)
            GameTooltip:AddLine("Click to switch to By Recipe view", 0.5, 0.8, 0.5)
        else
            GameTooltip:AddLine("Currently: By Recipe (per-recipe breakdown)", 0.7, 0.7, 0.7)
            GameTooltip:AddLine("Click to switch to Totals view", 0.5, 0.8, 0.5)
        end
        GameTooltip:Show()
    end

    sortModeBtn:SetScript("OnClick", function(self)
        local currentMode = GetConfig("materialSortMode") or "totals"
        local newMode = currentMode == "totals" and "byRecipe" or "totals"
        Dispatch("SET_CONFIG", { key = "materialSortMode", value = newMode })
        UpdateSortModeAppearance()
        HDG_RecipesTab:RefreshMaterials()
        -- Update tooltip if still hovering
        if GameTooltip:IsOwned(self) then
            ShowSortModeTooltip(self)
        end
    end)

    sortModeBtn:SetScript("OnEnter", function(self)
        ShowSortModeTooltip(self)
    end)
    sortModeBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    mainFrame.sortModeUpdate = UpdateSortModeAppearance
    UpdateSortModeAppearance()

    -- Power Crafting toggle button (Direct | Raw)
    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 directBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    directBtn:SetSize(40, 16)
    directBtn:SetPoint("LEFT", 1, 0)
    directBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
        tile = false
    })
    directBtn:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.5)

    local directText = directBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    directText:SetPoint("CENTER", 0, 0)
    ApplyFont(directText, scheme, "small")
    directText:SetText(directLabel)

    local rawBtn = CreateFrame("Button", nil, toggleContainer, "BackdropTemplate")
    rawBtn:SetSize(40, 16)
    rawBtn:SetPoint("LEFT", directBtn, "RIGHT", 1, 0)
    rawBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        edgeSize = 1,
        tile = false
    })
    rawBtn:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.5)

    local rawText = rawBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    rawText:SetPoint("CENTER", 0, 0)
    ApplyFont(rawText, scheme, "small")
    rawText:SetText(rawLabel)

    -- Update toggle button appearance based on state
    local function UpdateToggleAppearance()
        local showRaw = GetConfig("showRawMaterials")
        local s = GetScheme()
        local activeBtn, inactiveBtn = showRaw and rawBtn or directBtn, showRaw and directBtn or rawBtn
        local activeText, inactiveText = showRaw and rawText or directText, showRaw and directText or rawText
        activeBtn:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, s.button_hover.a)
        activeBtn:SetBackdropBorderColor(s.accent.r, s.accent.g, s.accent.b, 0.8)
        inactiveBtn:SetBackdropColor(s.panel.r, s.panel.g, s.panel.b, 0.8)
        inactiveBtn:SetBackdropBorderColor(s.border.r, s.border.g, s.border.b, 0.5)
        activeText:SetTextColor(s.text.r, s.text.g, s.text.b, s.text.a)
        inactiveText:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, s.text_dim.a)
        materialsTitle:SetText(showRaw and rawMatsLabel or requiredMatsLabel)
    end

    directBtn:SetScript("OnClick", function()
        Dispatch("SET_CONFIG", { key = "showRawMaterials", value = false })
        UpdateToggleAppearance()
        HDG_RecipesTab:RefreshMaterials()
        -- Also update pinned lumber widget if visible
        if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
            HDG_LumberTracker.RefreshPinned()
        end
    end)

    rawBtn:SetScript("OnClick", function()
        Dispatch("SET_CONFIG", { key = "showRawMaterials", value = true })
        UpdateToggleAppearance()
        HDG_RecipesTab:RefreshMaterials()
        -- Also update pinned lumber widget if visible
        if HDG_LumberTracker and HDG_LumberTracker.IsPinnedVisible and HDG_LumberTracker.IsPinnedVisible() then
            HDG_LumberTracker.RefreshPinned()
        end
    end)

    directBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Direct Materials|r", 1, 1, 1)
        GameTooltip:AddLine("Show materials as listed in recipes", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Crafted reagents shown as-is", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    directBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    rawBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Raw Materials (Power Craft)|r", 1, 1, 1)
        GameTooltip:AddLine("Expand crafted reagents to base materials", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Shows what you actually need to gather/buy", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    rawBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    mainFrame.materialsToggleUpdate = UpdateToggleAppearance
    UpdateToggleAppearance()

    materialsHeaderBg:EnableMouse(true)
    materialsHeaderBg:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(HDG.Theme.C_Text() .. "Required Materials|r", 1, 1, 1)
        GameTooltip:AddLine("All materials for your Crafting Queue", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Direct vs Raw:|r", 1, 1, 1)
        GameTooltip:AddLine("Direct: Materials as listed in recipes", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Raw: Expand crafted reagents to base materials", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "How to use:|r", 1, 1, 1)
        GameTooltip:AddLine("Right-click materials to search Auction House", 0.7, 0.7, 0.7)
        GameTooltip:AddLine("Use 'Add All to Auctionator' for bulk search", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    materialsHeaderBg:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Scroll frame
    local scroll, content = CreateScrollPane(materialsFrame, CONFIG.MATERIAL_ITEM_WIDTH, { bottom = 68 })
    mainFrame.materialsContent = content
    mainFrame.materialsScroll = scroll

    -- 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 -- Store reference for updating

    local clickButton = CreateFrame("Button", nil, addAllBar)
    clickButton:SetAllPoints(addAllBar)
    clickButton:RegisterForClicks("LeftButtonUp")
    clickButton:SetScript("OnClick", function()
        HDG_RecipesTab:AddAllMaterialsToAuctionator()
    end)
    clickButton:SetScript("OnEnter", function(self)
        addAllBar:SetStatusBarColor(0.15, 0.40, 0.40, 1.0) -- Brighter hover
        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
    return materialsFrame
end

-- ============================================================================
-- CREATE ALL PANELS
-- ============================================================================

function HDG_RecipesTab:Create()
    self:CreateRecipePanel()
    self:CreateShoppingPanel()
    self:CreateMaterialsPanel()
end

-- ============================================================================
-- SHOPPING LIST FUNCTIONS
-- ============================================================================

function HDG_RecipesTab:AddToShoppingList(recipe, skipRefresh)
    Dispatch("ADD_TO_SHOPPING_LIST", {
        item = {
            id = recipe.id,
            name = recipe.name,
            spell = recipe.spell,
            reagents = recipe.reagents,
            expansion = recipe.expansion or "Unknown",
            profession = recipe.profession,
            quantity = 1
        }
    })
    if not skipRefresh then
        HDG_UI.RefreshUI()
    end
end

-- ============================================================================
-- ROW POOL HELPERS (avoids frame churn on BAG_UPDATE every 0.3s)
-- ============================================================================

local function AcquireShoppingHeader()
    local h = table.remove(shoppingHeaderPool)
    if not h then
        h = CreateFrame("Frame", nil, mainFrame.shoppingContent)
        h.text = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        h.text:SetPoint("LEFT", 2, 0)
        h.text:SetJustifyH("LEFT")
    end
    h:SetParent(mainFrame.shoppingContent)
    h:ClearAllPoints()
    h:Show()
    table.insert(activeShoppingHeaders, h)
    return h
end

local function AcquireShoppingRow()
    local r = table.remove(shoppingRowPool)
    if not r then
        r = CreateFrame("Button", nil, mainFrame.shoppingContent, "BackdropTemplate")
        r:SetSize(CONFIG.SHOPPING_ITEM_WIDTH, CONFIG.SHOPPING_ITEM_HEIGHT)
        r:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        r:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        r.expandIcon = r:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        r.expandIcon:SetPoint("LEFT", 2, 0)
        r.profIcon = r:CreateTexture(nil, "ARTWORK")
        r.profIcon:SetSize(16, 16)
        r.nameText = r:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        r.nameText:SetJustifyH("LEFT")
        -- Craft button with decorations (pre-created, shown only when craftable)
        r.craftBtn = CreateFrame("Button", nil, r, "BackdropTemplate")
        r.craftBtn:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        r.craftBtn.highlight = r.craftBtn:CreateTexture(nil, "ARTWORK")
        r.craftBtn.highlight:SetTexture("Interface\\Buttons\\WHITE8x8")
        r.craftBtn.highlight:SetPoint("TOPLEFT", 1, -1)
        r.craftBtn.highlight:SetPoint("TOPRIGHT", -1, -1)
        r.craftBtn.highlight:SetHeight(6)
        r.craftBtn.shadow = r.craftBtn:CreateTexture(nil, "ARTWORK")
        r.craftBtn.shadow:SetTexture("Interface\\Buttons\\WHITE8x8")
        r.craftBtn.shadow:SetPoint("BOTTOMLEFT", 1, 1)
        r.craftBtn.shadow:SetPoint("BOTTOMRIGHT", -1, 1)
        r.craftBtn.shadow:SetHeight(4)
        r.craftBtn.glow = r.craftBtn:CreateTexture(nil, "BACKGROUND", nil, -1)
        r.craftBtn.glow:SetTexture("Interface\\Buttons\\WHITE8x8")
        r.craftBtn.glow:SetPoint("TOPLEFT", -2, 2)
        r.craftBtn.glow:SetPoint("BOTTOMRIGHT", 2, -2)
        r.craftBtn.btnText = r.craftBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    end
    r:SetParent(mainFrame.shoppingContent)
    r:ClearAllPoints()
    r:Show()
    r.expandIcon:Hide()
    r.craftBtn:Hide()
    table.insert(activeShoppingRows, r)
    return r
end

local function AcquireShoppingSub()
    local s = table.remove(shoppingSubPool)
    if not s then
        s = CreateFrame("Frame", nil, mainFrame.shoppingContent, "BackdropTemplate")
        s:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        s:EnableMouse(true)
        s.profIcon = s:CreateTexture(nil, "ARTWORK")
        s.profIcon:SetSize(14, 14)
        s.profIcon:SetPoint("LEFT", 4, 0)
        s.nameText = s:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        s.nameText:SetPoint("LEFT", 22, 0)
        s.nameText:SetJustifyH("LEFT")
        s.qtyText = s:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        s.qtyText:SetPoint("RIGHT", -4, 0)
        s.qtyText:SetJustifyH("RIGHT")
    end
    s:SetParent(mainFrame.shoppingContent)
    s:ClearAllPoints()
    s:Show()
    table.insert(activeShoppingSubs, s)
    return s
end

local function ReleaseAllShoppingRows()
    for _, h in ipairs(activeShoppingHeaders) do
        h:Hide(); h:ClearAllPoints()
        table.insert(shoppingHeaderPool, h)
    end
    activeShoppingHeaders = {}
    for _, r in ipairs(activeShoppingRows) do
        r:Hide(); r:ClearAllPoints()
        table.insert(shoppingRowPool, r)
    end
    activeShoppingRows = {}
    for _, s in ipairs(activeShoppingSubs) do
        s:Hide(); s:ClearAllPoints()
        table.insert(shoppingSubPool, s)
    end
    activeShoppingSubs = {}
end

local function AcquireMatHeader()
    local h = table.remove(matHeaderPool)
    if not h then
        h = CreateFrame("Button", nil, mainFrame.materialsContent, "BackdropTemplate")
        h:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        h.collapseIcon = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        h.collapseIcon:SetPoint("LEFT", 6, 0)
        h.icon = h:CreateTexture(nil, "ARTWORK")
        h.icon:SetSize(14, 14)
        h.icon:SetPoint("LEFT", 20, 0)
        h.headerText = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        h.headerText:SetPoint("LEFT", 40, 0)
        h.qtyText = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        h.qtyText:SetPoint("RIGHT", -8, 0)
    end
    h:SetParent(mainFrame.materialsContent)
    h:ClearAllPoints()
    h:Show()
    h.icon:Show()
    h.qtyText:Hide()
    table.insert(activeMatHeaders, h)
    return h
end

local function AcquireMatRow()
    local r = table.remove(matRowPool)
    if not r then
        r = CreateFrame("StatusBar", nil, mainFrame.materialsContent, "BackdropTemplate")
        r:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        r:SetStatusBarTexture(CONFIG.TEXTURES.STATUSBAR)
        r.nameText = r:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        r.nameText:SetJustifyH("LEFT")
        r.countText = r:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        r.clickBtn = CreateFrame("Button", nil, r)
        r.clickBtn:SetAllPoints(r)
        r.clickBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    end
    r:SetParent(mainFrame.materialsContent)
    r:ClearAllPoints()
    r:Show()
    r.nameText:ClearAllPoints()
    r.countText:ClearAllPoints()
    table.insert(activeMatRows, r)
    return r
end

local function AcquireMatSub()
    local s = table.remove(matSubPool)
    if not s then
        s = CreateFrame("Frame", nil, mainFrame.materialsContent, "BackdropTemplate")
        s:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        s:EnableMouse(true)
        s.icon = s:CreateTexture(nil, "ARTWORK")
        s.nameText = s:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        s.nameText:SetJustifyH("LEFT")
        s.countText = s:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        s.profIcon = s:CreateTexture(nil, "ARTWORK")
        s.profIcon:SetSize(14, 14)
    end
    s:SetParent(mainFrame.materialsContent)
    s:ClearAllPoints()
    s:Show()
    s.icon:Hide()
    s.profIcon:Hide()
    s.nameText:ClearAllPoints()
    s.countText:ClearAllPoints()
    table.insert(activeMatSubs, s)
    return s
end

local function ReleaseAllMatRows()
    for _, h in ipairs(activeMatHeaders) do
        h:Hide(); h:ClearAllPoints()
        table.insert(matHeaderPool, h)
    end
    activeMatHeaders = {}
    for _, r in ipairs(activeMatRows) do
        r:Hide(); r:ClearAllPoints()
        table.insert(matRowPool, r)
    end
    activeMatRows = {}
    for _, s in ipairs(activeMatSubs) do
        s:Hide(); s:ClearAllPoints()
        table.insert(matSubPool, s)
    end
    activeMatSubs = {}
end

local function RenderShoppingItem(itemData, yOffset, scheme)
    local item = itemData.item
    local i = itemData.index

    -- Check if this item has subsidiary recipes (crafted sub-components)
    local hasSubsidiaries = HDG_PowerCrafting.HasSubsidiaryRecipes(item)
    local isExpanded = hasSubsidiaries and expandedShoppingItems[item.id]

    local frame = AcquireShoppingRow()
    frame:SetPoint("TOPLEFT", CONFIG.SPACING.ITEM_PADDING, yOffset)
    frame:SetBackdropColor(unpack(GetShoppingItemColor(item)))

    -- Check if player knows recipe and can craft it
    local isKnown = item.spell and Data().PlayerKnowsRecipeBySpellID and
    Data().PlayerKnowsRecipeBySpellID(item.spell)
    local canCraft, maxCraftable = HDG_Data.CanCraftRecipe(item)
    maxCraftable = maxCraftable or 0

    -- Always reserve space for hammer icon to maintain consistent alignment
    local CRAFT_BTN_WIDTH = 44                        -- Width for "Craft (n)" button
    local PROF_ICON_SIZE = 16                         -- Profession icon size
    local EXPAND_ICON_WIDTH = hasSubsidiaries and 14 or 0 -- Space for expand/collapse indicator

    -- Expand/collapse indicator (pre-created, show/hide)
    if hasSubsidiaries then
        frame.expandIcon:Show()
        ApplyFont(frame.expandIcon, scheme, "small")
        frame.expandIcon:SetText(isExpanded and (HDG.Theme.C_Warning() .. "-|r") or (HDG.Theme.C_Warning() .. "+|r"))
        if scheme then
            frame.expandIcon:SetTextColor(scheme.warning.r, scheme.warning.g, scheme.warning.b, scheme.warning.a)
        end
    end

    -- Profession icon (fallback to cached index for legacy items without profession field)
    local profession = item.profession or GetProfessionForItem(item.id)
    frame.profIcon:ClearAllPoints()
    frame.profIcon:SetPoint("LEFT", 4 + EXPAND_ICON_WIDTH, 0)
    HDG_ExpansionData.ApplyProfessionIcon(frame.profIcon, profession)

    frame.nameText:ClearAllPoints()
    frame.nameText:SetPoint("LEFT", 4 + EXPAND_ICON_WIDTH + PROF_ICON_SIZE + 4, 0)
    frame.nameText:SetPoint("RIGHT", -CRAFT_BTN_WIDTH, 0)
    ApplyFont(frame.nameText, scheme, "small")
    if scheme then
        frame.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    end
    local localizedQueueName = Data().GetLocalizedName(item.id, item.name)
    frame.nameText:SetText(string.format("%dx %s", item.quantity, localizedQueueName))

    -- Craft button (only if known and craftable)
    if isKnown and canCraft then
        -- Determine button text based on craftable count
        local craftText = maxCraftable > 99 and "99+" or tostring(maxCraftable)
        local btnWidth = 34 + (string.len(craftText) > 1 and 6 or 0) + (string.len(craftText) > 2 and 4 or 0)

        local craftBtn = frame.craftBtn
        craftBtn:Show()
        craftBtn:ClearAllPoints()
        craftBtn:SetSize(btnWidth, 16)
        craftBtn:SetPoint("RIGHT", -2, 0)

        -- Bright green base color (success state)
        if scheme then
            craftBtn:SetBackdropColor(scheme.success.r, scheme.success.g, scheme.success.b, 1)
            craftBtn:SetBackdropBorderColor(scheme.success.r * 0.7, scheme.success.g * 0.7, scheme.success.b * 0.7, 1)
        else
            craftBtn:SetBackdropColor(0.15, 0.7, 0.15, 1)
            craftBtn:SetBackdropBorderColor(0.1, 0.5, 0.1, 1)
        end

        -- Top highlight for 3D bevel effect
        local highlight = craftBtn.highlight
        highlight:SetGradient("VERTICAL", CreateColor(0.5, 1, 0.5, 0.4), CreateColor(0.2, 0.8, 0.2, 0))

        -- Bottom shadow for 3D depth
        local shadow = craftBtn.shadow
        shadow:SetGradient("VERTICAL", CreateColor(0, 0.3, 0, 0.6), CreateColor(0.1, 0.5, 0.1, 0))

        -- Glow border effect
        local glow = craftBtn.glow
        glow:SetVertexColor(0.3, 1, 0.3, 0.3)

        -- Button text with count
        local btnText = craftBtn.btnText
        btnText:ClearAllPoints()
        ApplyFont(btnText, scheme, "small", 9)
        btnText:SetPoint("CENTER", 0, 0)
        btnText:SetText(string.format(HDG.Theme.C_Text() .. "%s|r", craftText))

        craftBtn:SetScript("OnEnter", function(self)
            -- Brighter on hover
            self:SetBackdropColor(0.2, 0.85, 0.2, 1)
            self:SetBackdropBorderColor(0.15, 0.65, 0.15, 1)
            glow:SetVertexColor(0.4, 1, 0.4, 0.5)
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:SetText(HDG.Theme.C_Success() .. "Craft Now|r", 1, 1, 1)
            GameTooltip:AddLine(string.format("%s", localizedQueueName), 1, 1, 1)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(string.format("Queued: " .. HDG.Theme.C_Warning() .. "%d|r", item.quantity), 0.7, 0.7, 0.7)
            GameTooltip:AddLine(string.format("Can craft: " .. HDG.Theme.C_Success() .. "%d|r", maxCraftable), 0.7, 0.7, 0.7)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Click|r to craft with confirmation", 0.7, 0.7, 0.7)
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Shift-Click|r to open in profession window", 0.7, 0.7, 0.7)
            GameTooltip:Show()
        end)
        craftBtn:SetScript("OnLeave", function(self)
            -- Normal state
            self:SetBackdropColor(0.15, 0.7, 0.15, 1)
            self:SetBackdropBorderColor(0.1, 0.5, 0.1, 1)
            glow:SetVertexColor(0.3, 1, 0.3, 0.3)
            GameTooltip:Hide()
        end)
        craftBtn:SetScript("OnMouseDown", function(self)
            -- Pressed state - darker, invert 3D effect
            self:SetBackdropColor(0.1, 0.5, 0.1, 1)
            highlight:SetGradient("VERTICAL", CreateColor(0, 0.3, 0, 0.4), CreateColor(0.1, 0.5, 0.1, 0))
            shadow:SetGradient("VERTICAL", CreateColor(0.3, 0.8, 0.3, 0.3), CreateColor(0.1, 0.5, 0.1, 0))
            btnText:SetPoint("CENTER", 0, -1)
        end)
        craftBtn:SetScript("OnMouseUp", function(self)
            -- Reset to normal
            self:SetBackdropColor(0.2, 0.85, 0.2, 1)
            highlight:SetGradient("VERTICAL", CreateColor(0.5, 1, 0.5, 0.4), CreateColor(0.2, 0.8, 0.2, 0))
            shadow:SetGradient("VERTICAL", CreateColor(0, 0.3, 0, 0.6), CreateColor(0.1, 0.5, 0.1, 0))
            btnText:SetPoint("CENTER", 0, 0)
        end)
        craftBtn:SetScript("OnClick", function()
            if item.spell then
                if IsShiftKeyDown() then
                    -- Shift-click: Open in profession window only (no craft)
                    -- Need to call twice: first opens profession, second navigates to recipe
                    if C_TradeSkillUI and C_TradeSkillUI.OpenRecipe then
                        C_TradeSkillUI.OpenRecipe(item.spell)
                        C_Timer.After(0.2, function()
                            C_TradeSkillUI.OpenRecipe(item.spell)
                        end)
                    end
                else
                    -- Normal click: Open profession window first, then show confirmation
                    local isTradeSkillOpen = C_TradeSkillUI and C_TradeSkillUI.IsTradeSkillReady and
                    C_TradeSkillUI.IsTradeSkillReady()
                    if isTradeSkillOpen then
                        -- Already open, but call OpenRecipe to navigate to correct recipe
                        C_TradeSkillUI.OpenRecipe(item.spell)
                        C_Timer.After(0.1, function()
                            HDG_RecipesTab.ShowCraftConfirmation(item, i)
                        end)
                    else
                        -- Open profession window first, then show confirmation
                        if C_TradeSkillUI and C_TradeSkillUI.OpenRecipe then
                            C_TradeSkillUI.OpenRecipe(item.spell)
                            C_Timer.After(0.3, function()
                                -- Call again to navigate to the recipe
                                C_TradeSkillUI.OpenRecipe(item.spell)
                                C_Timer.After(0.1, function()
                                    HDG_RecipesTab.ShowCraftConfirmation(item, i)
                                end)
                            end)
                        end
                    end
                end
            end
        end)
    end

    frame:SetScript("OnClick", function(_, button)
        if button == "RightButton" then
            if IsShiftKeyDown() then
                -- Shift+Right-click: Remove all
                Dispatch("REMOVE_FROM_SHOPPING_LIST", { itemID = item.id })
                expandedShoppingItems[item.id] = nil -- Clean up expanded state
                HDG_UI.RefreshUI()
            elseif hasSubsidiaries then
                -- Right-click: Toggle subsidiary recipes
                expandedShoppingItems[item.id] = not expandedShoppingItems[item.id]
                HDG_UI.RefreshUI()
            else
                -- No subsidiaries, right-click removes all
                Dispatch("REMOVE_FROM_SHOPPING_LIST", { itemID = item.id })
                HDG_UI.RefreshUI()
            end
        elseif IsShiftKeyDown() and item.spell then
            -- Shift+Left-click: Open in profession window
            if C_TradeSkillUI and C_TradeSkillUI.OpenRecipe then
                C_TradeSkillUI.OpenRecipe(item.spell)
                C_Timer.After(0.2, function()
                    C_TradeSkillUI.OpenRecipe(item.spell)
                end)
            end
        else
            -- Left-click: Remove one
            local newQty = item.quantity - 1
            Dispatch("UPDATE_SHOPPING_ITEM_QTY", { itemID = item.id, quantity = newQty })
            if newQty <= 0 then
                expandedShoppingItems[item.id] = nil -- Clean up expanded state
            end
            HDG_UI.RefreshUI()
        end
    end)
    frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    frame:SetScript("OnEnter", function(self)
        self:SetBackdropColor(0.3, 0.3, 0.35, 0.8)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)

        -- Build tooltip content function (called after item info loads)
        local function BuildTooltipContent()
            if item.reagents and #item.reagents > 0 then
                GameTooltip:AddLine(string.format(HDG.Theme.C_Text() .. "Materials (x%d):|r", item.quantity))
                for _, reagent in ipairs(item.reagents) do
                    local have = HDG_Data.GetItemCountWithVariants(reagent.id)
                    local need = (reagent.amount or 1) * item.quantity
                    local reagentName = Data().GetCachedItemName(reagent.id) or ("Item " .. reagent.id)
                    local color = have >= need and HDG.Theme.C_Success() or (have > 0 and HDG.Theme.C_Warning() or HDG.Theme.C_Error())
                    GameTooltip:AddDoubleLine("  " .. reagentName, string.format("%s%d/%d|r", color, have, need),
                        1, 1, 1, 1, 1, 1)
                end
                GameTooltip:AddLine(" ")
            end

            GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Left-click:|r Remove one", 0.7, 0.7, 0.7)
            if item.spell then
                GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Shift+Left-click:|r Open in profession window", 0.7, 0.7, 0.7)
            end
            if hasSubsidiaries then
                GameTooltip:AddLine(
                HDG.Theme.C_Warning() .. "Right-click:|r " .. (isExpanded and "Collapse" or "Show sub-recipes"), 0.7, 0.7, 0.7)
                GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Shift+Right-click:|r Remove all", 0.7, 0.7, 0.7)
            else
                GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Right-click:|r Remove all", 0.7, 0.7, 0.7)
            end
            GameTooltip:Show()
        end

        -- Use item hyperlink for instant tooltip (avoids async delay)
        if item.id then
            local _, itemLink = C_Item.GetItemInfo(item.id)
            if itemLink then
                GameTooltip:SetHyperlink(itemLink)
                GameTooltip:AddLine(" ")
                BuildTooltipContent()
            else
                GameTooltip:SetItemByID(item.id)
                C_Timer.After(0.05, function()
                    if GameTooltip:IsShown() and GameTooltip:GetOwner() == self then
                        GameTooltip:AddLine(" ")
                        BuildTooltipContent()
                    end
                end)
            end
        else
            GameTooltip:SetText(HDG.Theme.C_Accent() .. (item.name or "Unknown") .. "|r")
            BuildTooltipContent()
        end
    end)
    frame:SetScript("OnLeave", function(self)
        self:SetBackdropColor(unpack(GetShoppingItemColor(item)))
        GameTooltip:Hide()
    end)

    yOffset = yOffset - (CONFIG.SHOPPING_ITEM_HEIGHT + 2)

    -- Render subsidiary recipes if expanded
    if isExpanded then
        local subsidiaries = HDG_PowerCrafting.GetSubsidiaryRecipes(item)
        local SUB_INDENT = 16
        local SUB_HEIGHT = 18

        for _, sub in ipairs(subsidiaries) do
            local subFrame = AcquireShoppingSub()
            subFrame:SetSize(CONFIG.SHOPPING_ITEM_WIDTH - SUB_INDENT, SUB_HEIGHT)
            subFrame:SetPoint("TOPLEFT", CONFIG.SPACING.ITEM_PADDING + SUB_INDENT, yOffset)
            subFrame:SetBackdropColor(0.12, 0.12, 0.15, 0.7)

            -- Profession icon
            HDG_ExpansionData.ApplyProfessionIcon(subFrame.profIcon, sub.profession)

            -- Sub-recipe name
            ApplyFont(subFrame.nameText, scheme, "small")
            if scheme then
                subFrame.nameText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
            end
            subFrame.nameText:SetText(sub.name or ("Item " .. sub.itemID))

            -- Quantity
            ApplyFont(subFrame.qtyText, scheme, "small")
            subFrame.qtyText:SetText("x" .. sub.qty)
            if scheme then
                subFrame.qtyText:SetTextColor(scheme.warning.r, scheme.warning.g, scheme.warning.b, scheme.warning.a)
            end

            -- Tooltip with reagents
            subFrame:SetScript("OnEnter", function(self)
                self:SetBackdropColor(0.2, 0.2, 0.25, 0.9)
                GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                GameTooltip:SetText(sub.name or ("Item " .. sub.itemID), 1, 1, 1)
                if sub.profession then
                    GameTooltip:AddLine(sub.profession .. (sub.expansion and (" - " .. sub.expansion) or ""), 0.7,
                        0.7, 0.7)
                end
                if sub.slots and #sub.slots > 0 then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine("Reagents per craft:", 0.98, 0.84, 0.34)
                    for _, slot in ipairs(sub.slots) do
                        -- Only show basic (required) slots
                        if slot.type == "basic" and slot.itemID then
                            local reagentName = slot.name or
                            (HDG_PowerCrafting and HDG_PowerCrafting.GetItemName(slot.itemID)) or
                            ("Item " .. slot.itemID)
                            GameTooltip:AddDoubleLine("  " .. reagentName, "x" .. (slot.qty or 1), 0.8, 0.8, 0.8,
                                0.98, 0.84, 0.34)
                        end
                    end
                end
                GameTooltip:Show()
            end)
            subFrame:SetScript("OnLeave", function(self)
                self:SetBackdropColor(0.12, 0.12, 0.15, 0.7)
                GameTooltip:Hide()
            end)

            yOffset = yOffset - (SUB_HEIGHT + 1)
        end
    end

    return yOffset
end

local function GroupShoppingByCharacter(shoppingList)
    local currentCharKey = HDG_Data.GetCharacterKey()
    local currentCharShort = currentCharKey:match("^([^-]+)") or currentCharKey
    local itemsByCharacter = {}
    local characterOrder = {} -- Track order: current char first, then alts alphabetically

    for i, item in ipairs(shoppingList) do
        local crafters = {}
        if item.spell then
            crafters = HDG_Data.GetCharactersWhoKnowRecipe(item.spell, item.profession)
        end

        -- Determine primary crafter (current char > first alt > unknown)
        local primaryCrafter = "Unknown"
        if #crafters > 0 then
            -- Current character takes priority
            for _, c in ipairs(crafters) do
                if c.isCurrent then
                    primaryCrafter = currentCharKey
                    break
                end
            end
            -- If not current, use first alt
            if primaryCrafter == "Unknown" then
                primaryCrafter = crafters[1].name
            end
        end

        -- Add to group
        if not itemsByCharacter[primaryCrafter] then
            itemsByCharacter[primaryCrafter] = {}
            table.insert(characterOrder, primaryCrafter)
        end
        table.insert(itemsByCharacter[primaryCrafter], { item = item, index = i })
    end

    -- Sort character order: current char first, then alphabetically, "Unknown" last
    table.sort(characterOrder, function(a, b)
        if a == currentCharKey then return true end
        if b == currentCharKey then return false end
        if a == "Unknown" then return false end
        if b == "Unknown" then return true end
        return a < b
    end)

    return currentCharKey, currentCharShort, itemsByCharacter, characterOrder
end

function HDG_RecipesTab:RefreshShoppingList()
    if not mainFrame or not mainFrame.shoppingContent then return end
    ReleaseAllShoppingRows()
    local scheme = GetScheme()

    -- Update title with item count
    local shoppingList = GetShoppingList()
    if mainFrame.shoppingTitle and mainFrame.craftingQueueLabel then
        local count = #shoppingList
        if count > 0 then
            mainFrame.shoppingTitle:SetText(mainFrame.craftingQueueLabel .. " " .. HDG.Theme.C_Accent() .. "(" .. count .. ")|r")
        else
            mainFrame.shoppingTitle:SetText(mainFrame.craftingQueueLabel)
        end
    end

    if mainFrame.pslBtn then
        mainFrame.pslBtn:SetShown(IsPSLAvailable() and #shoppingList > 0)
    end

    if #shoppingList == 0 then return end

    local currentCharKey, currentCharShort, itemsByCharacter, characterOrder = GroupShoppingByCharacter(shoppingList)

    local yOffset = 0

    -- Render items grouped by character
    for _, charKey in ipairs(characterOrder) do
        local items = itemsByCharacter[charKey]

        -- Section header (pooled)
        local headerFrame = AcquireShoppingHeader()
        headerFrame:SetSize(CONFIG.SHOPPING_ITEM_WIDTH, 16)
        headerFrame:SetPoint("TOPLEFT", CONFIG.SPACING.ITEM_PADDING, yOffset)

        ApplyFont(headerFrame.text, scheme, "small")

        local charName = charKey:match("^([^-]+)") or charKey
        local realmName = charKey:match("-(.+)$") or ""
        if charKey == currentCharKey then
            headerFrame.text:SetText(HDG.Theme.C_Success() .. charName .. "|r " .. HDG.Theme.C_Dim() .. "- " .. realmName .. "|r " .. HDG.Theme.C_Warning() .. "(you)|r")
        elseif charKey == "Unknown" then
            headerFrame.text:SetText(HDG.Theme.C_Error() .. "Not Known|r")
        else
            headerFrame.text:SetText(HDG.Theme.C_Warning() .. charName .. "|r " .. HDG.Theme.C_Dim() .. "- " .. realmName .. "|r")
        end

        yOffset = yOffset - 18

        -- Render items in this group
        for _, itemData in ipairs(items) do
            yOffset = RenderShoppingItem(itemData, yOffset, scheme)
        end
    end     -- end character groups loop
    mainFrame.shoppingContent:SetHeight(math.abs(yOffset))
end

-- ============================================================================
-- MATERIALS REFRESH (BY RECIPE MODE)
-- ============================================================================

function HDG_RecipesTab:RefreshMaterialsByRecipe(shoppingList)
    -- Use raw materials aggregation when showRawMaterials is enabled
    local recipes
    if GetConfig("showRawMaterials") and HDG_PowerCrafting then
        recipes = HDG_PowerCrafting.AggregateMaterialsByRecipeRaw(shoppingList)
    else
        recipes = HDG_PowerCrafting.AggregateMaterialsByRecipe(shoppingList)
    end
    local yOffset = 0
    local collapsedMaterials = GetUIState("collapsedMaterials") or {}
    local categoryOrder = { "vendor", "gather", "craft", "unknown" }
    local scheme = GetScheme()

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

        -- Recipe Header (parent level)
        local recipeHeader = AcquireMatHeader()
        recipeHeader:SetSize(CONFIG.MATERIAL_ITEM_WIDTH, CONFIG.SPACING.HEADER_HEIGHT + 2)
        recipeHeader:SetPoint("TOPLEFT", 0, yOffset)
        recipeHeader:SetBackdropColor(scheme.accent.r * 0.3, scheme.accent.g * 0.3, scheme.accent.b * 0.3, 0.9)

        ApplyFont(recipeHeader.collapseIcon, scheme, "header")
        recipeHeader.collapseIcon:SetText(isRecipeCollapsed and (HDG.Theme.C_Accent() .. "+|r") or (HDG.Theme.C_Accent() .. "-|r"))

        -- Use headerText for the recipe name (icon not needed for recipe headers)
        recipeHeader.icon:Hide()
        recipeHeader.headerText:ClearAllPoints()
        recipeHeader.headerText:SetPoint("LEFT", 22, 0)
        ApplyFont(recipeHeader.headerText, scheme, "small")
        recipeHeader.headerText:SetWidth(170)
        recipeHeader.headerText:SetJustifyH("LEFT")
        recipeHeader.headerText:SetText(HDG.Theme.C_Text() .. recipeName .. "|r")

        -- Show quantity text
        recipeHeader.qtyText:Show()
        ApplyFont(recipeHeader.qtyText, scheme, "small")
        recipeHeader.qtyText:SetText(HDG.Theme.C_Warning() .. "x" .. recipeQty .. "|r")

        recipeHeader:SetScript("OnClick", function()
            Dispatch("SET_COLLAPSED_MATERIAL", { category = recipeKey, collapsed = not isRecipeCollapsed })
            HDG_RecipesTab:RefreshMaterials()
        end)

        recipeHeader:SetScript("OnEnter", function(self)
            self:SetBackdropColor(scheme.accent.r * 0.5, scheme.accent.g * 0.5, scheme.accent.b * 0.5, 1.0)
        end)
        recipeHeader:SetScript("OnLeave", function(self)
            self:SetBackdropColor(scheme.accent.r * 0.3, scheme.accent.g * 0.3, scheme.accent.b * 0.3, 0.9)
        end)

        yOffset = yOffset - (CONFIG.SPACING.HEADER_HEIGHT + 4)

        if not isRecipeCollapsed then
            -- Categorize this recipe's materials
            local categorized = HDG_PowerCrafting.CategorizeMaterialsForRecipe(recipeEntry.materials)

            for _, catKey in ipairs(categoryOrder) do
                local category = categorized[catKey]
                if category and #category.items > 0 then
                    -- Source Sub-Header (indented)
                    local subHeader = AcquireMatSub()
                    subHeader:SetSize(CONFIG.MATERIAL_ITEM_WIDTH - 8, 18)
                    subHeader:SetPoint("TOPLEFT", 8, yOffset)
                    subHeader:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.7)

                    subHeader.icon:SetSize(12, 12)
                    subHeader.icon:ClearAllPoints()
                    subHeader.icon:SetPoint("LEFT", 4, 0)
                    subHeader.icon:SetTexture(category.icon)
                    subHeader.icon:Show()

                    subHeader.nameText:SetPoint("LEFT", 20, 0)
                    ApplyFont(subHeader.nameText, scheme, "small")
                    subHeader.nameText:SetText(HDG.Theme.C_Dim() .. category.name .. "|r")

                    yOffset = yOffset - 20

                    -- Material rows for this category
                    for _, mat in ipairs(category.items) do
                        local row = AcquireMatRow()
                        row:SetSize(CONFIG.MATERIAL_ITEM_WIDTH - 16, CONFIG.MATERIAL_ITEM_HEIGHT - 2)
                        row:SetPoint("TOPLEFT", 12, yOffset)
                        row:SetMinMaxValues(0, mat.needed)
                        row:SetValue(math.min(mat.have, mat.needed))

                        local percentage = mat.needed > 0 and (mat.have / mat.needed * 100) or 0
                        row:SetStatusBarColor(HDG_UIHelpers.GetProgressBarColor(percentage))

                        local expColor = HDG_ExpansionData.GetColor(mat.expansion)
                        if expColor then
                            row:SetBackdropColor(expColor[1], expColor[2], expColor[3], 0.15)
                        else
                            row:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.4)
                        end

                        local matName = Data().GetCachedItemName(mat.id)
                        row.nameText:SetPoint("LEFT", 4, 0)
                        row.nameText:SetWidth(130)
                        ApplyFont(row.nameText, scheme, "small")
                        row.nameText:SetText(matName)
                        row.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)

                        local countStr
                        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

                        row.countText:SetPoint("RIGHT", -6, 0)
                        ApplyFont(row.countText, scheme, "small")
                        row.countText:SetText(countStr)

                        -- Tooltip
                        local clickButton = row.clickBtn
                        clickButton:SetScript("OnEnter", function(self)
                            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                            GameTooltip:SetItemByID(mat.id)
                            GameTooltip:AddLine(" ")
                            GameTooltip:AddLine(HDG.Theme.C_Warning() .. "For: " .. recipeName .. " x" .. recipeQty .. "|r", 0.9, 0.9, 0.9)
                            GameTooltip:AddLine("Per craft: x" .. mat.perCraft, 0.7, 0.7, 0.7)
                            GameTooltip:Show()
                        end)
                        clickButton:SetScript("OnLeave", function() GameTooltip:Hide() end)

                        yOffset = yOffset - (CONFIG.MATERIAL_ITEM_HEIGHT - 1)
                    end
                    yOffset = yOffset - 4
                end
            end
            yOffset = yOffset - 4
        end
    end

    if yOffset == 0 then
        if not mainFrame.materialsEmptyText then
            mainFrame.materialsEmptyText = mainFrame.materialsContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            mainFrame.materialsEmptyText:SetPoint("CENTER", 0, -50)
        end
        ApplyFont(mainFrame.materialsEmptyText, scheme, "body")
        mainFrame.materialsEmptyText:SetText(HDG.Theme.C_Dim() .. "Add recipes to crafting queue|r")
        mainFrame.materialsEmptyText:Show()
        if mainFrame.materialsCostText then
            mainFrame.materialsCostText:SetText("")
        end
    else
        -- Calculate estimated cost for By Recipe mode
        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

    mainFrame.materialsContent:SetHeight(math.abs(yOffset) + 20)
end

-- ============================================================================
-- MATERIALS REFRESH HELPERS (TOTALS MODE)
-- ============================================================================

local function RenderMaterialCategory(catKey, category, yOffset, scheme, collapsedMaterials)
    local isCollapsed = collapsedMaterials[catKey]

    -- Category Header
    local header = AcquireMatHeader()
    header:SetSize(CONFIG.MATERIAL_ITEM_WIDTH, CONFIG.SPACING.HEADER_HEIGHT)
    header:SetPoint("TOPLEFT", 0, yOffset)
    header:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 1.0)

    ApplyFont(header.collapseIcon, scheme, "header")
    header.collapseIcon:SetText(isCollapsed and (HDG.Theme.C_Warning() .. "+|r") or (HDG.Theme.C_Warning() .. "-|r"))

    header.icon:SetTexture(category.icon)
    header.icon:Show()

    ApplyFont(header.headerText, scheme, "small")
    header.headerText:SetText(category.name)
    header.headerText:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b)

    header:SetScript("OnClick", function()
        Dispatch("SET_COLLAPSED_MATERIAL", { category = catKey, collapsed = not isCollapsed })
        HDG_RecipesTab:RefreshMaterials()
    end)

    header:SetScript("OnEnter", function(self) self:SetBackdropColor(scheme.button_hover.r, scheme.button_hover.g, scheme.button_hover.b, 1.0) end)
    header:SetScript("OnLeave", function(self) self:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 1.0) end)

    yOffset = yOffset - (CONFIG.SPACING.HEADER_HEIGHT + 2)

    if not isCollapsed then
        for _, mat in ipairs(category.items) do
            local row = AcquireMatRow()
            row:SetSize(CONFIG.MATERIAL_ITEM_WIDTH, CONFIG.MATERIAL_ITEM_HEIGHT)
            row:SetPoint("TOPLEFT", 4, yOffset)
            row:SetMinMaxValues(0, mat.needed)
            row:SetValue(math.min(mat.have, mat.needed))

            local percentage = mat.needed > 0 and (mat.have / mat.needed * 100) or 0
            row:SetStatusBarColor(HDG_UIHelpers.GetProgressBarColor(percentage))

            local expColor = HDG_ExpansionData.GetColor(mat.expansion)
            if expColor then
                row:SetBackdropColor(expColor[1], expColor[2], expColor[3], 0.15)
            else
                row:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.5)
            end

            local matName = Data().GetCachedItemName(mat.id)
            row.nameText:SetPoint("LEFT", 4, 0)
            row.nameText:SetWidth(150)
            ApplyFont(row.nameText, scheme, "small")
            row.nameText:SetText(matName)
            row.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)

            local countStr
            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

            row.countText:SetPoint("RIGHT", -10, 0)
            ApplyFont(row.countText, scheme, "small")
            row.countText:SetText(countStr)

            -- Store price for total
            local matPrice = Data().GetItemPrice(mat.id)
            if matPrice then
                local stillNeeded = math.max(0, mat.needed - mat.have)
                mat.estimatedCost = matPrice * stillNeeded
            end

            -- Click handler
            local clickButton = row.clickBtn
            clickButton:SetScript("OnClick", function(_, button)
                local itemName = matName
                if button == "LeftButton" and not IsShiftKeyDown() and not IsControlKeyDown() 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 == "LeftButton" and IsShiftKeyDown() then
                    if C_AddOns.IsAddOnLoaded("Auctionator") and Auctionator and Auctionator.API and Auctionator.API.v1 and Auctionator.API.v1.MultiSearch then
                        Auctionator.API.v1.MultiSearch("HousingDecorGuide", { itemName })
                        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r Searching Auctionator for:", 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.GetShoppingListItems then
                            local success, items = pcall(Auctionator.API.v1.GetShoppingListItems, 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
                elseif button == "LeftButton" and IsControlKeyDown() then
                    -- Ctrl+Click to track reagent for gathering (only for gathered items)
                    if catKey == "gather" and HDG_LumberTracker and HDG_LumberTracker.TrackReagent then
                        HDG_LumberTracker.TrackReagent(mat.id, itemName, mat.needed)
                    end
                end
            end)

            clickButton:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                GameTooltip:SetItemByID(mat.id)
                GameTooltip:AddLine(" ")
                GameTooltip:AddLine("Used in:", 1, 1, 1)
                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
                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 search in Auctionator/TSM", 0.7, 0.7, 0.7)
                if catKey == "gather" then
                    GameTooltip:AddLine(HDG.Theme.C_Success() .. "Ctrl-Click|r to track for gathering", 0.7, 0.7, 0.7)
                end
                GameTooltip:AddLine(HDG.Theme.C_Success() .. "Right-Click|r to add to Auctionator search list", 0.7, 0.7, 0.7)
                GameTooltip:Show()
            end)
            clickButton:SetScript("OnLeave", function() GameTooltip:Hide() end)

            yOffset = yOffset - (CONFIG.MATERIAL_ITEM_HEIGHT + 1)
        end
        yOffset = yOffset - 6
    end
    return yOffset
end

local function RenderIntermediatesSection(intermediates, yOffset, scheme, collapsedMaterials)
    -- Convert to sorted array
    local intermediateArray = {}
    for _, inter in pairs(intermediates) do
        if inter.needed > 0 then
            table.insert(intermediateArray, inter)
        end
    end

    if #intermediateArray > 0 then
        -- Sort by name
        table.sort(intermediateArray, function(a, b)
            return (a.name or "") < (b.name or "")
        end)

        local isCollapsed = collapsedMaterials["intermediates"]

        -- Intermediates Header
        local header = AcquireMatHeader()
        header:SetSize(CONFIG.MATERIAL_ITEM_WIDTH, CONFIG.SPACING.HEADER_HEIGHT)
        header:SetPoint("TOPLEFT", 0, yOffset)
        if scheme then
            header:SetBackdropColor(scheme.button_active.r * 0.5, scheme.button_active.g * 0.6, scheme.button_active.b * 0.6, 0.9)
        else
            header:SetBackdropColor(0.15, 0.25, 0.25, 0.9)
        end

        ApplyFont(header.collapseIcon, scheme, "header")
        header.collapseIcon:SetText(isCollapsed and (HDG.Theme.C_Accent() .. "+|r") or (HDG.Theme.C_Accent() .. "-|r"))

        header.icon:SetTexture("Interface\\Icons\\Trade_Engineering")
        header.icon:Show()

        ApplyFont(header.headerText, scheme, "small")
        header.headerText:SetText(HDG.Theme.C_Accent() .. "Craft These First|r")

        header:SetScript("OnClick", function()
            Dispatch("SET_COLLAPSED_MATERIAL", { category = "intermediates", collapsed = not isCollapsed })
            HDG_RecipesTab:RefreshMaterials()
        end)

        header:SetScript("OnEnter", function(self)
            if scheme then
                self:SetBackdropColor(scheme.button_hover.r, scheme.button_hover.g, scheme.button_hover.b, 0.9)
            else
                self:SetBackdropColor(0.2, 0.35, 0.35, 0.9)
            end
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Craft These First|r", 1, 1, 1)
            GameTooltip:AddLine("Intermediate items you need to craft", 0.7, 0.7, 0.7)
            GameTooltip:AddLine("before making the final decor.", 0.7, 0.7, 0.7)
            GameTooltip:Show()
        end)
        header:SetScript("OnLeave", function(self)
            if scheme then
                self:SetBackdropColor(scheme.button_active.r * 0.5, scheme.button_active.g * 0.6, scheme.button_active.b * 0.6, 0.9)
            else
                self:SetBackdropColor(0.15, 0.25, 0.25, 0.9)
            end
            GameTooltip:Hide()
        end)

        yOffset = yOffset - (CONFIG.SPACING.HEADER_HEIGHT + 2)

        if not isCollapsed then
            for _, inter in ipairs(intermediateArray) do
                -- Intermediate item row (parent)
                local row = AcquireMatSub()
                row:SetSize(CONFIG.MATERIAL_ITEM_WIDTH, CONFIG.MATERIAL_ITEM_HEIGHT)
                row:SetPoint("TOPLEFT", 4, yOffset)
                if scheme then
                    row:SetBackdropColor(scheme.panel.r, scheme.panel.g * 1.2, scheme.panel.b * 1.2, 0.6)
                else
                    row:SetBackdropColor(0.1, 0.2, 0.2, 0.6)
                end

                -- Look up profession from ProfessionsDB for this intermediate
                local interProfession = nil
                local interRecipe = GetRecipeByItemID(inter.id)
                if interRecipe then
                    interProfession = interRecipe.profession
                end

                -- Profession icon
                local PROF_ICON_SIZE = 14
                row.profIcon:ClearAllPoints()
                row.profIcon:SetPoint("LEFT", 4, 0)
                row.profIcon:Show()
                HDG_ExpansionData.ApplyProfessionIcon(row.profIcon, interProfession)

                local interName = inter.name or ("Item #" .. inter.id)
                row.nameText:SetPoint("LEFT", 4 + PROF_ICON_SIZE + 4, 0)
                row.nameText:SetWidth(145)
                ApplyFont(row.nameText, scheme, "small")
                row.nameText:SetText(interName)
                if scheme then
                    row.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
                end

                local countStr = string.format("x%d", inter.needed)
                row.countText:SetPoint("RIGHT", -10, 0)
                ApplyFont(row.countText, scheme, "small")
                row.countText:SetText(countStr)
                if scheme then
                    row.countText:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b, scheme.accent.a)
                end

                row:SetScript("OnEnter", function(self)
                    self:SetBackdropColor(0.15, 0.3, 0.3, 0.8)
                    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)

                    -- Show recipe being used and alternatives
                    if HDG_PowerCrafting and HDG_PowerCrafting.GetAllRecipesForItem then
                        local allRecipes = HDG_PowerCrafting.GetAllRecipesForItem(inter.id)
                        if #allRecipes > 0 then
                            -- Show the preferred recipe
                            local preferred = allRecipes[1]
                            if preferred then
                                local profColor = HDG.Theme.C_Warning() -- Gold for profession
                                GameTooltip:AddLine(" ")
                                GameTooltip:AddLine(
                                "Via: " ..
                                (preferred.name or "Unknown") ..
                                " " .. profColor .. "(" .. (preferred.profession or "?") .. ")|r", 0.9, 0.9, 0.9)

                                -- Show materials for this recipe
                                if preferred.slots then
                                    for _, slot in ipairs(preferred.slots) do
                                        if slot.type == "basic" and slot.itemID then
                                            local matName = slot.name or HDG_PowerCrafting.GetItemName(slot.itemID) or
                                            ("Item #" .. slot.itemID)
                                            GameTooltip:AddDoubleLine("  " .. matName, "x" .. (slot.qty or 1), 0.7,
                                                0.7, 0.7, 0.8, 0.8, 0.8)
                                        end
                                    end
                                end
                            end

                            -- Show alternatives if any
                            if #allRecipes > 1 then
                                GameTooltip:AddLine(" ")
                                GameTooltip:AddLine("Alternative 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
                        -- Fallback: show children if no recipe info available
                        GameTooltip:AddLine("Requires:", 0.7, 0.7, 0.7)
                        for childID, 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
                    GameTooltip:Show()
                end)
                row:SetScript("OnLeave", function(self)
                    self:SetBackdropColor(0.1, 0.2, 0.2, 0.6)
                    GameTooltip:Hide()
                end)

                yOffset = yOffset - (CONFIG.MATERIAL_ITEM_HEIGHT + 1)

                -- Show children (raw materials for this intermediate) indented
                if inter.children and next(inter.children) then
                    for childID, childData in pairs(inter.children) do
                        -- Skip finishing reagents (should already be filtered, but double-check)
                        if childData.source == "Ignore" then
                            -- Skip this child entirely
                        else
                            local childRow = AcquireMatSub()
                            childRow:SetSize(CONFIG.MATERIAL_ITEM_WIDTH - 12, CONFIG.MATERIAL_ITEM_HEIGHT - 2)
                            childRow:SetPoint("TOPLEFT", 16, yOffset)
                            childRow:SetBackdropColor(0.08, 0.12, 0.12, 0.4)

                            local childName = childData.name or ("Item #" .. childID)
                            childRow.nameText:SetPoint("LEFT", 8, 0)
                            childRow.nameText:SetWidth(140)
                            ApplyFont(childRow.nameText, scheme, "small")
                            childRow.nameText:SetText("- " .. childName)
                            if scheme then
                                childRow.nameText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
                            end

                            local totalChildNeeded = (tonumber(childData.perCraft) or 1) *
                            (tonumber(inter.needed) or 1)
                            childRow.countText:SetPoint("RIGHT", -8, 0)
                            ApplyFont(childRow.countText, scheme, "small")
                            childRow.countText:SetText("x" .. totalChildNeeded)
                            if scheme then
                                childRow.countText:SetTextColor(scheme.text_dim.r * 0.8, scheme.text_dim.g * 0.8, scheme.text_dim.b * 0.8, scheme.text_dim.a)
                            end

                            local tooltipItemID = childData
                            .id                            -- Use stored ID, not key (key is name after consolidation)
                            childRow:SetScript("OnEnter", function(self)
                                self:SetBackdropColor(0.12, 0.18, 0.18, 0.6)
                                GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
                                if tooltipItemID and type(tooltipItemID) == "number" and tooltipItemID > 0 then
                                    GameTooltip:SetItemByID(tooltipItemID)
                                else
                                    GameTooltip:SetText(childData.name or "Unknown Item", 1, 1, 1)
                                end
                                GameTooltip:Show()
                            end)
                            childRow:SetScript("OnLeave", function(self)
                                self:SetBackdropColor(0.08, 0.12, 0.12, 0.4)
                                GameTooltip:Hide()
                            end)

                            yOffset = yOffset - (CONFIG.MATERIAL_ITEM_HEIGHT - 1)
                        end -- end else (not Ignore)
                    end
                end
            end
            yOffset = yOffset - 6
        end
    end
    return yOffset
end

-- ============================================================================
-- MATERIALS REFRESH (TOTALS MODE)
-- ============================================================================

function HDG_RecipesTab:RefreshMaterials()
    if not mainFrame or not mainFrame.materialsContent then return end
    ReleaseAllMatRows()
    -- Hide persistent empty text if it exists
    if mainFrame.materialsEmptyText then mainFrame.materialsEmptyText:Hide() end
    local scheme = GetScheme()

    -- Update Auctionator button text based on mode
    if mainFrame.addAllAuctionatorText then
        if 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 shoppingList = GetShoppingList()
    local sortMode = GetConfig("materialSortMode") or "totals"

    -- BY RECIPE MODE: Show materials grouped by recipe first, then by source
    if sortMode == "byRecipe" then
        self:RefreshMaterialsByRecipe(shoppingList)
        return
    end

    -- TOTALS MODE: Original aggregated view
    local materials, intermediates
    if GetConfig("showRawMaterials") and HDG_PowerCrafting then
        -- Power Crafting mode: expand crafted reagents to raw materials
        materials, intermediates = HDG_PowerCrafting.AggregateRawMaterials(shoppingList)
    else
        -- Direct mode: show materials as listed in recipes
        materials = HDG_PowerCrafting.AggregateMaterials(shoppingList)
        intermediates = nil
    end
    local sortedMaterials = HDG_PowerCrafting.FilterAndSortMaterials(materials)
    local categorized = HDG_PowerCrafting.CategorizeMaterials(sortedMaterials)

    local yOffset = 0
    local categoryOrder = { "vendor", "gather", "craft", "unknown" }

    local collapsedMaterials = GetUIState("collapsedMaterials") or {}
    for _, catKey in ipairs(categoryOrder) do
        local category = categorized[catKey]
        if category and #category.items > 0 then
            yOffset = RenderMaterialCategory(catKey, category, yOffset, scheme, collapsedMaterials)
        end
    end

    -- Show intermediates section in Raw mode
    if intermediates and GetConfig("showRawMaterials") then
        yOffset = RenderIntermediatesSection(intermediates, yOffset, scheme, collapsedMaterials)
    end

    if yOffset == 0 then
        if not mainFrame.materialsEmptyText then
            mainFrame.materialsEmptyText = mainFrame.materialsContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            mainFrame.materialsEmptyText:SetPoint("CENTER", 0, -50)
        end
        ApplyFont(mainFrame.materialsEmptyText, scheme, "body")
        mainFrame.materialsEmptyText:SetText(HDG.Theme.C_Dim() .. "Add recipes to crafting queue|r")
        mainFrame.materialsEmptyText:Show()
        if mainFrame.materialsCostText then
            mainFrame.materialsCostText:SetText("")
        end
    else
        -- Calculate estimated cost for Totals mode
        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

    mainFrame.materialsContent:SetHeight(math.abs(yOffset) + 20)
end

-- ============================================================================
-- ADD ALL MATERIALS TO AUCTIONATOR
-- ============================================================================

function HDG_RecipesTab:AddAllMaterialsToAuctionator()
    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 shoppingList = GetShoppingList()

    -- Check which mode we're in (Direct vs Raw)
    if GetConfig("showRawMaterials") and HDG_PowerCrafting then
        -- Raw mode: get raw materials only (not intermediates)
        -- In Raw mode, we want the base materials, not crafted sub-components
        materials = HDG_PowerCrafting.AggregateRawMaterials(shoppingList)
        modeLabel = "raw"
    else
        -- Direct mode: get materials as listed in recipes
        materials = HDG_PowerCrafting.AggregateMaterials(shoppingList)
        modeLabel = "direct"
    end

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

    local callerID = "HousingDecorGuide"
    local listName = "Housing Decor"

    -- Check if ConvertToSearchString is available for quantity support
    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" category since those are intermediates the player will craft
    -- In Direct mode, include all categories
    local categoryOrder
    if modeLabel == "raw" then
        categoryOrder = { "vendor", "gather", "unknown" } -- Exclude "craft" in raw mode
    else
        categoryOrder = { "vendor", "gather", "craft", "unknown" }
    end

    local skippedCount = 0 -- Track items skipped because player has enough
    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
                    -- Calculate how many more the player needs (subtract what they have)
                    local totalNeeded = mat.needed or mat.total or 1
                    local playerHas = Data().GetItemCountWithVariants(mat.id) or 0
                    local stillNeeded = math.max(0, totalNeeded - playerHas)

                    -- Only add to list if player still needs some
                    if stillNeeded > 0 then
                        local searchString
                        if hasQuantitySupport then
                            -- Use the new API with quantity support
                            local term = {
                                searchString = itemName,
                                isExact = true,
                                quantity = stillNeeded,
                            }
                            searchString = Auctionator.API.v1.ConvertToSearchString(callerID, term)
                        else
                            -- Fallback to plain item name
                            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
        -- CreateShoppingList replaces the list entirely, so we always create fresh with current quantities
        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

-- ============================================================================
-- REFRESH PROFESSION TABS
-- ============================================================================

local professionList = { "Alchemy", "Blacksmithing", "Cooking", "Enchanting", "Engineering",
    "Inscription", "Jewelcrafting", "Leatherworking", "Tailoring" }

local COLLECTION_COL_RIGHT = -6
local RECIPES_COL_RIGHT = -68
local COL_WIDTH = 60

local function EnsureProfessionTabFrames()
    if mainFrame.profTabsCreated then return end

    local scheme = GetScheme()
    local yOffset = 0

    -- "All Professions" button
    local allBtn = CreateFrame("Button", nil, mainFrame.professionTabs, "BackdropTemplate")
    allBtn:SetSize(CONFIG.LEFT_PANEL_WIDTH - 16, 18)
    allBtn:SetPoint("TOPLEFT", 0, yOffset)
    allBtn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    allBtn:SetBackdropColor(scheme.button_normal.r, scheme.button_normal.g, scheme.button_normal.b, 0.2)

    local allHighlight = allBtn:CreateTexture(nil, "HIGHLIGHT")
    allHighlight:SetAllPoints()
    allHighlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
    allHighlight:SetBlendMode("ADD")
    allHighlight:Hide()

    local allText = allBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    allText:SetPoint("LEFT", 6, 0)
    ApplyFont(allText, scheme, "small")

    allBtn.highlight = allHighlight
    allBtn.textLabel = allText
    mainFrame.allProfBtn = allBtn

    allBtn:SetScript("OnClick", function()
        HDG_Data.SetProfessionFilter("All")
        HDG_UI.RefreshUI()
    end)
    allBtn:SetScript("OnEnter", function(self)
        if not self.isSelected then
            local s = GetScheme()
            self:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, 0.4)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        local label = L["FILTER_ALL_PROFESSIONS"] or "All Professions"
        GameTooltip:AddLine(label, 1, 0.84, 0)
        GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Show recipes from all professions|r")
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Tip:|r Use search to find recipes", 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    allBtn:SetScript("OnLeave", function(self)
        local s = GetScheme()
        if self.isSelected then
            self:SetBackdropColor(s.accent.r, s.accent.g, s.accent.b, 0.3)
        else
            self:SetBackdropColor(s.button_normal.r, s.button_normal.g, s.button_normal.b, 0.2)
        end
        GameTooltip:Hide()
    end)

    yOffset = yOffset - 20

    -- Separator line after "All Professions"
    local separator = mainFrame.professionTabs:CreateTexture(nil, "ARTWORK")
    separator:SetHeight(1)
    separator:SetPoint("TOPLEFT", 0, yOffset - 2)
    separator:SetPoint("TOPRIGHT", 0, yOffset - 2)
    separator:SetColorTexture(0.3, 0.3, 0.35, 0.5)
    mainFrame.profSeparator = separator
    yOffset = yOffset - 6

    -- Per-profession buttons
    mainFrame.profBtns = {}
    for _, profName in ipairs(professionList) do
        local btn = CreateFrame("Button", nil, mainFrame.professionTabs, "BackdropTemplate")
        btn:SetSize(CONFIG.LEFT_PANEL_WIDTH - 16, 18)
        btn:SetPoint("TOPLEFT", 0, yOffset)
        btn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
        btn:SetBackdropColor(scheme.button_normal.r, scheme.button_normal.g, scheme.button_normal.b, 0.2)

        local highlight = btn:CreateTexture(nil, "HIGHLIGHT")
        highlight:SetAllPoints()
        highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
        highlight:SetBlendMode("ADD")
        highlight:Hide()

        local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        text:SetPoint("LEFT", 6, 0)
        ApplyFont(text, scheme, "small")

        local collectionBadge = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        collectionBadge:SetPoint("RIGHT", COLLECTION_COL_RIGHT, 0)
        collectionBadge:SetWidth(COL_WIDTH)
        collectionBadge:SetJustifyH("LEFT")
        ApplyFont(collectionBadge, scheme, "small")

        local recipesBadge = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        recipesBadge:SetPoint("RIGHT", RECIPES_COL_RIGHT, 0)
        recipesBadge:SetWidth(COL_WIDTH)
        recipesBadge:SetJustifyH("LEFT")
        ApplyFont(recipesBadge, scheme, "small")

        btn.highlight = highlight
        btn.textLabel = text
        btn.collectionBadge = collectionBadge
        btn.recipesBadge = recipesBadge
        btn.professionName = profName

        btn:SetScript("OnClick", function(self)
            HDG_Data.SetProfessionFilter(self.professionName)
            HDG_UI.RefreshUI()
        end)
        btn:SetScript("OnEnter", function(self)
            if not self.isSelected then
                local s = GetScheme()
                self:SetBackdropColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, 0.4)
            end
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:AddLine(self.localizedProfName or profName, 1, 0.84, 0)
            GameTooltip:AddLine(self.hasProf and (HDG.Theme.C_Success() .. "You have this profession|r") or
                HDG.Theme.C_Dim() .. "You don't have this profession|r")
            GameTooltip:AddLine(" ")
            if self.hasProf then
                GameTooltip:AddLine(
                    string.format(HDG.Theme.C_Accent() .. "Recipes Known:|r %d / %d (%.0f%%)", self.recipesKnown or 0, self.totalCount or 0,
                        self.recipesPercentage or 0), 1, 1, 1)
            elseif self.altCharacter then
                GameTooltip:AddLine(HDG.Theme.C_Dim() .. "Known by alts|r", 0.78, 0.78, 0.78)
            end
            GameTooltip:AddLine(
                string.format(HDG.Theme.C_Warning() .. "Decor Collected:|r %d / %d (%.0f%%)", self.collectedCount or 0, self.totalCount or 0,
                    self.collectionPercentage or 0), 1, 1, 1)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function(self)
            local s = GetScheme()
            if self.isSelected then
                self:SetBackdropColor(s.accent.r, s.accent.g, s.accent.b, 0.3)
            else
                self:SetBackdropColor(s.button_normal.r, s.button_normal.g, s.button_normal.b, 0.2)
            end
            GameTooltip:Hide()
        end)

        mainFrame.profBtns[profName] = btn
        yOffset = yOffset - 20
    end

    mainFrame.profTabsCreated = true
end

function HDG_RecipesTab:RefreshProfessionTabs()
    if not mainFrame or not mainFrame.professionTabs then return end

    EnsureProfessionTabFrames()

    local currentFilter = HDG_Data.GetProfessionFilter()
    local filtescheme = GetScheme()
    local allProfsLabel = L["FILTER_ALL_PROFESSIONS"] or "All Professions"

    -- Update "All Professions" button
    local allBtn = mainFrame.allProfBtn
    local isAllSelected = (currentFilter == "All")
    allBtn.isSelected = isAllSelected
    if isAllSelected then
        allBtn:SetBackdropColor(filtescheme.accent.r, filtescheme.accent.g, filtescheme.accent.b, 0.3)
        allBtn.highlight:SetVertexColor(1, 1, 1, 0.5); allBtn.highlight:Show()
        allBtn.textLabel:SetText(HDG.Theme.C_Warning() .. allProfsLabel .. "|r")
    else
        allBtn:SetBackdropColor(filtescheme.button_normal.r, filtescheme.button_normal.g, filtescheme.button_normal.b, 0.2)
        allBtn.highlight:Hide()
        allBtn.textLabel:SetText(HDG.Theme.C_Text() .. allProfsLabel .. "|r")
    end

    -- Update per-profession buttons (also accumulate grand totals in one pass)
    local grandTotalRecipes, grandKnownRecipes, grandTotalDecor, grandCollectedDecor = 0, 0, 0, 0
    local D = Data()
    for _, profName in ipairs(professionList) do
        local btn = mainFrame.profBtns[profName]
        local localizedProfName = HDG_ExpansionData.GetLocalizedProfessionName(profName)
        local hasProf = D.PlayerHasProfession(profName)
        local recipesKnown, collectedCount, totalCount = 0, 0, 0

        for _, profData in ipairs(privateDB.professions or {}) do
            if profData.name == profName and profData.items then
                for _, recipe in ipairs(profData.items) do
                    totalCount = totalCount + 1
                    if D.PlayerKnowsRecipeBySpellID and D.PlayerKnowsRecipeBySpellID(recipe.spell) then
                        recipesKnown = recipesKnown + 1
                    end
                    if D.IsItemCollected(recipe.id) then
                        collectedCount = collectedCount + 1
                    end
                end
                break
            end
        end
        grandTotalRecipes = grandTotalRecipes + totalCount
        grandTotalDecor = grandTotalDecor + totalCount
        grandCollectedDecor = grandCollectedDecor + collectedCount
        if hasProf then
            grandKnownRecipes = grandKnownRecipes + recipesKnown
        else
            local altKnown = D.GetBestAltRecipeCount(profName)
            if altKnown > 0 then grandKnownRecipes = grandKnownRecipes + altKnown end
        end

        local recipesPercentage = totalCount > 0 and (recipesKnown / totalCount * 100) or 0
        local collectionPercentage = totalCount > 0 and (collectedCount / totalCount * 100) or 0
        local isSelected = (currentFilter == profName)
        btn.isSelected = isSelected

        if isSelected then
            btn:SetBackdropColor(filtescheme.accent.r, filtescheme.accent.g, filtescheme.accent.b, 0.3)
            btn.highlight:SetVertexColor(1, 1, 1, 0.5); btn.highlight:Show()
        else
            btn:SetBackdropColor(filtescheme.button_normal.r, filtescheme.button_normal.g, filtescheme.button_normal.b, 0.2)
            btn.highlight:Hide()
        end

        btn.textLabel:SetText(localizedProfName)

        local collectionColor = collectionPercentage >= 100 and HDG.Theme.C_Success() or HDG.Theme.C_Warning()
        btn.collectionBadge:SetText(string.format("%s%d/%d|r", collectionColor, collectedCount, totalCount))

        local altKnown, altTotal, altChar = D.GetBestAltRecipeCount(profName)
        if hasProf then
            local recipesColor = recipesPercentage >= 100 and HDG.Theme.C_Success() or HDG.Theme.C_Accent()
            btn.recipesBadge:SetText(string.format("%s%d/%d|r", recipesColor, recipesKnown, totalCount))
        elseif altKnown > 0 then
            btn.recipesBadge:SetText(string.format(HDG.Theme.C_Text() .. "%d/%d|r", altKnown, altTotal))
            btn.altCharacter = altChar
        else
            btn.recipesBadge:SetText(HDG.Theme.C_Dim() .. "--|r")
            btn.altCharacter = nil
        end

        local profColor
        if isSelected or hasProf then
            profColor = filtescheme.text_recipe_known or {r=0.80, g=0.29, b=0.09, a=1}
        elseif altKnown > 0 then
            profColor = filtescheme.text_recipe_alt or {r=1, g=1, b=1, a=1}
        else
            profColor = filtescheme.text_recipe_no_prof or {r=0.56, g=0.56, b=0.56, a=1}
        end
        btn.textLabel:SetTextColor(profColor.r, profColor.g, profColor.b, profColor.a)

        btn.localizedProfName = localizedProfName
        btn.hasProf = hasProf
        btn.recipesKnown = recipesKnown
        btn.collectedCount = collectedCount
        btn.totalCount = totalCount
        btn.recipesPercentage = recipesPercentage
        btn.collectionPercentage = collectionPercentage
    end

    local collectionPct = grandTotalDecor > 0 and (grandCollectedDecor / grandTotalDecor * 100) or 0
    local recipesPct = grandTotalRecipes > 0 and (grandKnownRecipes / grandTotalRecipes * 100) or 0

    -- Update header percentages
    if mainFrame.headerCollectionPct then
        mainFrame.headerCollectionPct:SetText(string.format("%s%.0f%%|r",
            collectionPct >= 100 and HDG.Theme.C_Success() or HDG.Theme.C_Warning(), collectionPct))
    end
    if mainFrame.headerRecipesPct then
        mainFrame.headerRecipesPct:SetText(string.format("%s%.0f%%|r", recipesPct >= 100 and HDG.Theme.C_Success() or HDG.Theme.C_Accent(),
            recipesPct))
    end

    -- Set up tooltips on header frames with totals
    if mainFrame.recipesHeaderFrame then
        mainFrame.recipesHeaderFrame:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:AddLine(HDG.Theme.C_Accent() .. "Recipes Known|r", 1, 1, 1)
            GameTooltip:AddLine(
            string.format("Total: %d / %d (%.0f%%)", grandKnownRecipes, grandTotalRecipes, recipesPct), 0.9, 0.9, 0.9)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Includes recipes known by you and alts", 0.6, 0.6, 0.6)
            GameTooltip:Show()
        end)
        mainFrame.recipesHeaderFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)
    end

    if mainFrame.collectionHeaderFrame then
        mainFrame.collectionHeaderFrame:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Decor Collected|r", 1, 1, 1)
            GameTooltip:AddLine(
            string.format("Total: %d / %d (%.0f%%)", grandCollectedDecor, grandTotalDecor, collectionPct), 0.9, 0.9, 0.9)
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Account-wide decor collection progress", 0.6, 0.6, 0.6)
            GameTooltip:Show()
        end)
        mainFrame.collectionHeaderFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)
    end

    -- Update title based on selection
    if mainFrame.recipeTitle then
        if HDG_Data.GetProfessionFilter() == "All" then
            mainFrame.recipeTitle:SetText(allProfsLabel)
        else
            mainFrame.recipeTitle:SetText("Professions")
        end
    end
end

-- ============================================================================
-- REFRESH RECIPES (content area)
-- ============================================================================

function HDG_RecipesTab:RefreshRecipes()
    if not mainFrame or not mainFrame.recipeContent then return end

    if mainFrame.UpdateCraftableButton then mainFrame.UpdateCraftableButton() end

    -- Release pooled recipe rows before clearing orphan FontStrings/textures
    if recipeRowPool then recipeRowPool:ReleaseAll(ResetRecipeRow) end
    craftabilityCache = {} -- clear per-refresh cache
    HDG_UIHelpers.ClearScrollContent(mainFrame.recipeContent)
    HideProfessionSectionHeader()
    self:RefreshProfessionTabs()

    local yOffset = 0
    local playerProfessions = Data().GetPlayerProfessions()
    local isAllMode = (HDG_Data.GetProfessionFilter() == "All")
    local isCraftableMode = HDG_Data.IsCraftableFilterEnabled()

    if #playerProfessions == 1 then
        -- Single profession selected - show header with profession name
        UpdateProfessionSectionHeader(playerProfessions[1].name)
        if mainFrame.stickyHeader then mainFrame.stickyHeader:Hide() end
    else
        -- Multiple professions (All mode) - show header with localized "All Professions"
        local allProfsLabel = L["FILTER_ALL_PROFESSIONS"] or "All Professions"
        UpdateProfessionSectionHeader(allProfsLabel)
        if mainFrame.stickyHeader then mainFrame.stickyHeader:Hide() end
    end

    -- Special case: All Professions + Ready mode = unified tier list (no profession headers)
    if isAllMode and isCraftableMode then
        -- Collect all recipes from all professions into unified tiers
        local tier100, tier75to99, tier50to74, tierBelow50 = {}, {}, {}, {}

        for _, profData in ipairs(playerProfessions) do
            local profName = profData.name
            local hasProfession = HDG_Data.PlayerHasProfession(profName)

            for _, recipe in ipairs(profData.items) do
                if HDG_Data.RecipeMatchesFilters(recipe) then
                    local isKnown = HDG_Data.PlayerKnowsRecipeBySpellID(recipe.spell)
                    local canCraft, maxQty, _, matPercent = GetCachedCraftability(recipe)

                    recipe._maxCraftable = maxQty
                    recipe._materialPercent = matPercent
                    recipe._noProfession = not hasProfession
                    recipe._professionName = profName -- Track which profession for display

                    if hasProfession and isKnown and canCraft and maxQty > 0 then
                        table.insert(tier100, recipe)
                    elseif matPercent >= 75 then
                        table.insert(tier75to99, recipe)
                    elseif matPercent >= 50 then
                        table.insert(tier50to74, recipe)
                    elseif matPercent > 0 then
                        table.insert(tierBelow50, recipe)
                    end
                end
            end
        end

        table.sort(tier75to99, sortByPercent)
        table.sort(tier50to74, sortByPercent)
        table.sort(tierBelow50, sortByPercent)

        -- Render unified tiers (showProfessionIcon = true for All Professions mode)
        yOffset = RenderTierGroup(tier100, "Ready to Craft", HDG.Theme.C_Success(), yOffset, true)
        yOffset = RenderTierGroup(tier75to99, "Almost Ready (75-99%)", HDG.Theme.C_Success(), yOffset, true)
        yOffset = RenderTierGroup(tier50to74, "Halfway There (50-74%)", HDG.Theme.C_Warning(), yOffset, true)
        yOffset = RenderTierGroup(tierBelow50, "Need Materials (<50%)", HDG.Theme.C_Error(), yOffset, true)
    else
        -- Normal mode: render per profession
        for _, profData in ipairs(playerProfessions) do
            -- In "All" mode without craftable filter: show inline headers
            -- In single profession mode: skip inline headers, show fixed header
            local showInlineHeader = isAllMode and not isCraftableMode
            local skipFixedHeader = isAllMode
            yOffset = RenderProfession(profData, yOffset, showInlineHeader, skipFixedHeader)
        end
    end

    mainFrame.recipeContent:SetHeight(math.abs(yOffset) + 20)
end

-- ============================================================================
-- MAIN REFRESH (called by HDG_UI.RefreshUI)
-- ============================================================================

function HDG_RecipesTab:Refresh()
    self:RefreshRecipes()
    self:RefreshShoppingList()
    self:RefreshMaterials()
end

-- ============================================================================
-- SHOW/HIDE
-- ============================================================================

function HDG_RecipesTab:Show()
    if mainFrame.recipeFrame then mainFrame.recipeFrame:Show() end
    if mainFrame.shoppingFrame then mainFrame.shoppingFrame:Show() end
    if mainFrame.materialsFrame then mainFrame.materialsFrame:Show() end
    -- Initialize lumber layout based on pin state
    if HDG_UI and HDG_UI.UpdateLumberLayout then
        HDG_UI.UpdateLumberLayout()
    end
    self:Refresh()
end

function HDG_RecipesTab:Hide()
    if mainFrame.recipeFrame then mainFrame.recipeFrame:Hide() end
    if mainFrame.shoppingFrame then mainFrame.shoppingFrame:Hide() end
    if mainFrame.materialsFrame then mainFrame.materialsFrame:Hide() end
end

-- ============================================================================
-- EVENTBUS SUBSCRIPTIONS
-- Decouple UI refresh from direct function calls
-- ============================================================================

local function RegisterEventBusHandlers()
    if not HDG or not HDG.EventBus then return end

    HDG.EventBus:Register("HDG_STATE_CHANGED", function(payload)
        if not payload or not payload.action then return end

        local action = payload.action

        -- Shopping list changes: refresh shopping list and materials
        if action == "ADD_TO_SHOPPING_LIST" or
           action == "REMOVE_FROM_SHOPPING_LIST" or
           action == "UPDATE_SHOPPING_ITEM_QTY" or
           action == "CLEAR_SHOPPING_LIST" then
            HDG_RecipesTab:RefreshShoppingList()
            HDG_RecipesTab:RefreshMaterials()
        end

        -- Config changes that affect recipe display
        if action == "SET_CONFIG" then
            -- Could check payload for specific config keys if needed
            HDG_RecipesTab:RefreshRecipes()
        end

        -- UI filter changes
        if action == "SET_UI" then
            HDG_RecipesTab:RefreshRecipes()
        end
    end)
end

RegisterEventBusHandlers()

-- ============================================================================
-- EXPORT TO GLOBAL
-- ============================================================================

_G.HDG_RecipesTab = HDG_RecipesTab

return HDG_RecipesTab
