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()
    local HDG = _G.HousingDecorGuide
    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 = {}, {}

-- Lowercase name cache (avoids repeated :lower() calls during search filtering)
local recipeLowerNameCache = {}

local function GetRecipeLowerName(recipe)
    local key = recipe.recipeKey or recipe.name
    if not key then return "" end
    if not recipeLowerNameCache[key] then
        recipeLowerNameCache[key] = (recipe.name or ""):lower()
    end
    return recipeLowerNameCache[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
-- ============================================================================

-- Per-character profession filter helpers (uses HDG_Data.GetCharacterKey)

function HDG_RecipesTab.GetProfessionFilter()
    local charKey = HDG_Data.GetCharacterKey()
    local charFilters = GetUIState("characterProfessionFilter")
    if charKey and charFilters and charFilters[charKey] then
        return charFilters[charKey]
    end
    return "All"
end

function HDG_RecipesTab.SetProfessionFilter(professionName)
    local charKey = HDG_Data.GetCharacterKey()
    Dispatch("SET_CHARACTER_PROFESSION_FILTER", { charKey = charKey, filter = professionName })
end

-- Use centralized ClearScrollContent from UIHelpers
local function ClearScrollContent(scrollContent)
    HDG_UIHelpers.ClearScrollContent(scrollContent)
end

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

-- Get subsidiary recipes (crafted sub-components) for a shopping list item or intermediate
-- Returns array of { itemID, name, qty, profession, recipeID, slots }
-- Can accept either a shopping list item (with .reagents array) or a ProfessionsDB recipe (with .slots array)
local function GetSubsidiaryRecipes(item, multiplier)
    local subsidiaries = {}
    multiplier = multiplier or 1

    if not item then return subsidiaries end
    if not HDG_ProfessionsDB then return subsidiaries end

    -- Handle both shopping list format (array) and ProfessionsDB format (keyed table)
    local reagentsList = item.reagents
    if not reagentsList then return subsidiaries end

    -- Detect format: array vs keyed table
    local isArray = reagentsList[1] ~= nil

    if isArray then
        -- Shopping list format: array of { id, amount, name }
        for _, reagent in ipairs(reagentsList) do
            local reagentID = reagent.id
            local source = HDG_ReagentsDB and HDG_ReagentsDB[reagentID]

            if source == "Crafted" then
                local recipe = GetRecipeByItemID(reagentID)
                if recipe then
                    local qtyNeeded = (reagent.amount or reagent.qty or 1) * (item.quantity or 1) * multiplier
                    table.insert(subsidiaries, {
                        itemID = reagentID,
                        name = recipe.name or reagent.name or ("Item " .. reagentID),
                        qty = qtyNeeded,
                        profession = recipe.profession,
                        expansion = recipe.expansion,
                        recipeID = recipe.recipeID,
                        slots = recipe.slots,
                    })
                end
            end
        end
    else
        -- ProfessionsDB format: keyed table { [reagentID] = qty }
        for reagentID, qty in pairs(reagentsList) do
            local source = HDG_ReagentsDB and HDG_ReagentsDB[reagentID]

            if source == "Crafted" then
                local recipe = GetRecipeByItemID(reagentID)
                if recipe then
                    local qtyNeeded = qty * multiplier
                    table.insert(subsidiaries, {
                        itemID = reagentID,
                        name = recipe.name or ("Item " .. reagentID),
                        qty = qtyNeeded,
                        profession = recipe.profession,
                        expansion = recipe.expansion,
                        recipeID = recipe.recipeID,
                        slots = recipe.slots,
                    })
                end
            end
        end
    end

    -- Sort by profession then name
    table.sort(subsidiaries, function(a, b)
        if a.profession ~= b.profession then
            return (a.profession or "") < (b.profession or "")
        end
        return (a.name or "") < (b.name or "")
    end)

    return subsidiaries
end

-- Check if item has any subsidiary recipes (works with both formats)
local function HasSubsidiaryRecipes(item)
    if not item or not item.reagents then return false end

    local reagentsList = item.reagents
    local isArray = reagentsList[1] ~= nil

    if isArray then
        for _, reagent in ipairs(reagentsList) do
            local source = HDG_ReagentsDB and HDG_ReagentsDB[reagent.id]
            if source == "Crafted" and GetRecipeByItemID(reagent.id) then
                return true
            end
        end
    else
        for reagentID, _ in pairs(reagentsList) do
            local source = HDG_ReagentsDB and HDG_ReagentsDB[reagentID]
            if source == "Crafted" and GetRecipeByItemID(reagentID) then
                return true
            end
        end
    end
    return false
end

-- ============================================================================
-- MATERIALS FUNCTIONS
-- ============================================================================

-- Reference centralized lumber data
local LUMBER_IDS = HDG_ExpansionData.LUMBER_IDS

-- Aggregate materials from shopping list
function HDG_RecipesTab.AggregateMaterials(shoppingList)
    local materials = {}

    for _, item in ipairs(shoppingList) do
        if item.reagents then
            for reagentKey, reagent in pairs(item.reagents) do
                local matID = reagent.id or reagentKey

                -- Skip finishing reagents (optional quality boosters like Illustrious Insight)
                local reagentSource = HDG_ReagentsDB and HDG_ReagentsDB[matID]
                if reagentSource ~= "Ignore" then
                    if not materials[matID] then
                        local matSource = reagent.source
                        if (not matSource or matSource == "Unknown") and HDG_ReagentsDB then
                            matSource = HDG_ReagentsDB[matID] or "Unknown"
                        end

                        materials[matID] = {
                            id = matID,
                            needed = 0,
                            have = Data().GetItemCountWithVariants(matID),
                            sources = {},
                            expansion = item.expansion,
                            source = matSource
                        }
                    end

                    local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                    local amountForThis = reagentAmount * item.quantity
                    materials[matID].needed = materials[matID].needed + amountForThis

                    local recipeName = item.name or ("Item " .. (item.id or "?"))
                    if not materials[matID].sources[recipeName] then
                        materials[matID].sources[recipeName] = {
                            count = item.quantity,
                            perCraft = reagentAmount,
                            total = amountForThis
                        }
                    else
                        materials[matID].sources[recipeName].count = materials[matID].sources[recipeName].count +
                            item.quantity
                        materials[matID].sources[recipeName].total = materials[matID].sources[recipeName].total +
                            amountForThis
                    end

                    if item.expansion and materials[matID].expansion ~= item.expansion then
                        materials[matID].expansion = item.expansion
                    end
                end
            end
        end
    end

    return materials
end

-- Aggregate materials BY RECIPE (for "By Recipe" sort mode)
-- Returns: table of { recipeName, recipeId, quantity, categories = { vendor = {...}, gather = {...}, ... } }
function HDG_RecipesTab.AggregateMaterialsByRecipe(shoppingList)
    local recipes = {}

    for _, item in ipairs(shoppingList) do
        if item.reagents then
            local recipeName = item.name or ("Item " .. (item.id or "?"))
            local recipeEntry = {
                recipeName = recipeName,
                recipeId = item.id,
                quantity = item.quantity,
                expansion = item.expansion,
                materials = {}
            }

            for reagentKey, reagent in pairs(item.reagents) do
                local matID = reagent.id or reagentKey
                local reagentSource = HDG_ReagentsDB and HDG_ReagentsDB[matID]

                if reagentSource ~= "Ignore" then
                    local matSource = reagent.source
                    if (not matSource or matSource == "Unknown") and HDG_ReagentsDB then
                        matSource = HDG_ReagentsDB[matID] or "Unknown"
                    end

                    local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                    local amountForThis = reagentAmount * item.quantity

                    recipeEntry.materials[matID] = {
                        id = matID,
                        needed = amountForThis,
                        have = Data().GetItemCountWithVariants(matID),
                        perCraft = reagentAmount,
                        expansion = item.expansion,
                        source = matSource
                    }
                end
            end

            table.insert(recipes, recipeEntry)
        end
    end

    return recipes
end

-- Aggregate RAW materials BY RECIPE (for "By Recipe" sort mode with showRawMaterials=true)
-- Expands crafted reagents to their base components
function HDG_RecipesTab.AggregateMaterialsByRecipeRaw(shoppingList)
    local recipes = {}

    for _, item in ipairs(shoppingList) do
        if item.reagents and HDG_PowerCrafting then
            local recipeName = item.name or ("Item " .. (item.id or "?"))
            local recipeEntry = {
                recipeName = recipeName,
                recipeId = item.id,
                quantity = item.quantity,
                expansion = item.expansion,
                materials = {}
            }

            -- Convert shopping list reagents format for Power Crafting
            local reagentsForGraph = {}
            for reagentKey, reagent in pairs(item.reagents) do
                local reagentID = reagent.id or reagentKey
                local reagentQty = reagent.amount or reagent.qty or 1
                local reagentName = reagent.name or ("Item " .. reagentID)
                reagentsForGraph[reagentID] = { name = reagentName, qty = reagentQty }
            end

            -- Build dependency graph
            local pseudoRecipe = { reagents = reagentsForGraph }
            local graph = HDG_PowerCrafting.BuildDependencyGraph(pseudoRecipe)

            if graph and graph.rawMaterials then
                -- Process raw materials (leaf nodes from the graph)
                for matID, rawMat in pairs(graph.rawMaterials) do
                    local matSource = HDG_ReagentsDB and HDG_ReagentsDB[matID] or "Unknown"
                    if matSource ~= "Ignore" then
                        local qty = rawMat.qty * item.quantity
                        recipeEntry.materials[matID] = {
                            id = matID,
                            needed = qty,
                            have = Data().GetItemCountWithVariants(matID),
                            perCraft = rawMat.qty,
                            expansion = item.expansion,
                            source = matSource
                        }
                    end
                end
            else
                -- Fallback: no Power Crafting graph, use direct reagents
                for reagentKey, reagent in pairs(item.reagents) do
                    local matID = reagent.id or reagentKey
                    local reagentSource = HDG_ReagentsDB and HDG_ReagentsDB[matID]

                    if reagentSource ~= "Ignore" then
                        local matSource = reagent.source
                        if (not matSource or matSource == "Unknown") and HDG_ReagentsDB then
                            matSource = HDG_ReagentsDB[matID] or "Unknown"
                        end

                        local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                        local amountForThis = reagentAmount * item.quantity

                        recipeEntry.materials[matID] = {
                            id = matID,
                            needed = amountForThis,
                            have = Data().GetItemCountWithVariants(matID),
                            perCraft = reagentAmount,
                            expansion = item.expansion,
                            source = matSource
                        }
                    end
                end
            end

            table.insert(recipes, recipeEntry)
        end
    end

    return recipes
end

-- Categorize materials for a single recipe (used in By Recipe mode)
function HDG_RecipesTab.CategorizeMaterialsForRecipe(materials)
    local categories = {
        vendor = { name = "From Vendor", icon = "Interface\\Icons\\inv_misc_coin_01", items = {} },
        gather = { name = "From Gathering / AH", icon = "Interface\\Icons\\inv_misc_herb_01", items = {} },
        craft = { name = "From Crafting / AH", icon = "Interface\\Icons\\trade_blacksmithing", items = {} },
        unknown = { name = "Other", icon = "Interface\\Icons\\inv_misc_questionmark", items = {} }
    }

    local sourceMapping = {
        ["Vendor"] = "vendor",
        ["Gathering"] = "gather",
        ["Drop"] = "gather",
        ["Farming"] = "gather",
        ["Crafted"] = "craft",
        ["Unknown"] = "unknown"
    }

    for _, mat in pairs(materials) do
        local sourceStr = mat.source or "Unknown"
        local baseSource = sourceStr:match("^([^:]+)") or sourceStr
        local catKey = sourceMapping[baseSource] or "unknown"
        table.insert(categories[catKey].items, mat)
    end

    -- Sort items in each category by name
    for _, category in pairs(categories) do
        table.sort(category.items, function(a, b)
            local nameA = Data().GetCachedItemName(a.id) or ""
            local nameB = Data().GetCachedItemName(b.id) or ""
            return nameA < nameB
        end)
    end

    return categories
end

-- Aggregate raw materials using Power Crafting (expands crafted reagents)
-- Returns: materials table AND intermediates table (crafted sub-components)
function HDG_RecipesTab.AggregateRawMaterials(shoppingList)
    local materials = {}
    local intermediates = {} -- Crafted sub-components with their raw material children

    for _, item in ipairs(shoppingList) do
        if item.reagents and HDG_PowerCrafting then
            -- Convert shopping list reagents format to HDG_DecorDB format for Power Crafting
            -- Handle both array format (from Recipes tab) and keyed table format (from Preview tab)
            local reagentsForGraph = {}
            for reagentKey, reagent in pairs(item.reagents) do
                -- Array format: reagent = {id=123, name="...", amount=5}
                -- Keyed format: reagentKey = 123, reagent = {name="...", qty=5}
                local reagentID = reagent.id or reagentKey
                local reagentQty = reagent.amount or reagent.qty or 1
                local reagentName = reagent.name or ("Item " .. reagentID)
                reagentsForGraph[reagentID] = { name = reagentName, qty = reagentQty }
            end

            -- Build a pseudo-recipe for the graph builder
            local pseudoRecipe = { reagents = reagentsForGraph }

            -- Build dependency graph
            local graph = HDG_PowerCrafting.BuildDependencyGraph(pseudoRecipe)
            if graph then
                -- Process intermediate crafted items (depth > 0 means it's a sub-component)
                if graph.nodes then
                    for nodeID, node in pairs(graph.nodes) do
                        if node.isCrafted then
                            -- This is a crafted intermediate
                            local qty = node.qty * item.quantity
                            if not intermediates[nodeID] then
                                intermediates[nodeID] = {
                                    id = nodeID,
                                    name = node.name,
                                    needed = 0,
                                    have = Data().GetItemCountWithVariants(nodeID),
                                    source = "Crafted",
                                    expansion = item.expansion,
                                    children = {}, -- Raw materials needed for this
                                    sources = {}
                                }
                            end
                            intermediates[nodeID].needed = intermediates[nodeID].needed + qty

                            -- Track which decor recipes use this intermediate
                            local recipeName = item.name or ("Item " .. (item.id or "?"))
                            if not intermediates[nodeID].sources[recipeName] then
                                intermediates[nodeID].sources[recipeName] = { count = item.quantity, total = qty }
                            end

                            -- Find the raw materials that go into this intermediate
                            local craftRecipe = HDG_PowerCrafting.GetCraftedRecipe(nodeID)
                            if craftRecipe then
                                for subMatID, subQty in pairs(craftRecipe) do
                                    local subSource = HDG_ReagentsDB and HDG_ReagentsDB[subMatID] or "Unknown"
                                    -- Skip finishing reagents (optional quality boosters)
                                    if subSource ~= "Ignore" then
                                        local subName = HDG_PowerCrafting.GetItemName(subMatID)
                                        if not intermediates[nodeID].children[subMatID] then
                                            intermediates[nodeID].children[subMatID] = {
                                                id = subMatID,
                                                name = subName,
                                                perCraft = subQty,
                                                source = subSource
                                            }
                                        end
                                    end
                                end
                                -- Consolidate quality rank variants by name (R1/R2/R3 are alternatives, not additives)
                                local consolidatedChildren = {}
                                for childID, childData in pairs(intermediates[nodeID].children) do
                                    local name = childData.name or "Unknown"
                                    if consolidatedChildren[name] then
                                        -- Same-named items are quality rank alternatives - take max qty
                                        consolidatedChildren[name].perCraft = math.max(
                                            consolidatedChildren[name].perCraft,
                                            childData.perCraft
                                        )
                                    else
                                        consolidatedChildren[name] = {
                                            id = childID,
                                            name = name,
                                            perCraft = childData.perCraft,
                                            source = childData.source
                                        }
                                    end
                                end
                                intermediates[nodeID].children = consolidatedChildren
                            end
                        end
                    end
                end

                -- Process raw materials (leaf nodes)
                if graph.rawMaterials then
                    for matID, rawMat in pairs(graph.rawMaterials) do
                        local qty = rawMat.qty * item.quantity
                        if not materials[matID] then
                            materials[matID] = {
                                id = matID,
                                needed = 0,
                                have = Data().GetItemCountWithVariants(matID),
                                sources = {},
                                expansion = item.expansion,
                                source = rawMat.source
                            }
                        end
                        materials[matID].needed = materials[matID].needed + qty

                        local recipeName = item.name or ("Item " .. (item.id or "?"))
                        if not materials[matID].sources[recipeName] then
                            materials[matID].sources[recipeName] = {
                                count = item.quantity,
                                perCraft = rawMat.qty,
                                total = qty
                            }
                        else
                            materials[matID].sources[recipeName].count = materials[matID].sources[recipeName].count +
                            item.quantity
                            materials[matID].sources[recipeName].total = materials[matID].sources[recipeName].total + qty
                        end
                    end
                end
            else
                -- Fallback to direct materials if graph building fails
                for _, reagent in ipairs(item.reagents) do
                    local matID = reagent.id
                    if not materials[matID] then
                        local matSource = reagent.source
                        if (not matSource or matSource == "Unknown") and HDG_ReagentsDB then
                            matSource = HDG_ReagentsDB[matID] or "Unknown"
                        end
                        materials[matID] = {
                            id = matID,
                            needed = 0,
                            have = Data().GetItemCountWithVariants(matID),
                            sources = {},
                            expansion = item.expansion,
                            source = matSource
                        }
                    end
                    local reagentAmount = reagent.amount or reagent.qty or reagent.count or 1
                    local amountForThis = reagentAmount * item.quantity
                    materials[matID].needed = materials[matID].needed + amountForThis

                    local recipeName = item.name or ("Item " .. (item.id or "?"))
                    if not materials[matID].sources[recipeName] then
                        materials[matID].sources[recipeName] = {
                            count = item.quantity,
                            perCraft = reagentAmount,
                            total = amountForThis
                        }
                    end
                end
            end
        end
    end

    return materials, intermediates
end

-- Filter and sort materials (excludes lumber)
function HDG_RecipesTab.FilterAndSortMaterials(materials)
    local matArray = {}

    for _, mat in pairs(materials) do
        if not LUMBER_IDS[mat.id] then
            table.insert(matArray, mat)
        end
    end

    table.sort(matArray, function(a, b)
        if not a or not b then return false end
        local aName = Data().GetCachedItemName(a.id) or ""
        local bName = Data().GetCachedItemName(b.id) or ""
        return aName < bName
    end)

    return matArray
end

-- Categorize materials by source
function HDG_RecipesTab.CategorizeMaterials(matArray)
    local categories = {
        vendor = { name = L["MAT_FROM_VENDOR"] or "From Vendor", icon = "Interface\\Icons\\INV_Misc_Coin_02", items = {} },
        gather = { name = L["MAT_FROM_GATHERING"] or "From Gathering / AH", icon = "Interface\\Icons\\INV_Misc_Map_01", items = {} },
        craft = { name = L["MAT_FROM_CRAFTING"] or "From Crafting / AH", icon = "Interface\\Icons\\Trade_Engineering", items = {} },
        unknown = { name = "Other", icon = "Interface\\Icons\\INV_Misc_QuestionMark", items = {} }
    }

    local sourceToCategory = {
        ["Vendor"] = "vendor",
        ["Gathering"] = "gather",
        ["Crafted"] = "craft",
        ["Drop"] = "gather",
        ["Farming"] = "gather",
        ["Unknown"] = "unknown",
    }

    for _, mat in ipairs(matArray) do
        local source = mat.source or "Unknown"
        -- Handle "Vendor:XX" format (e.g., "Vendor:20" for vendor price)
        local baseSource = source:match("^([^:]+)") or source
        local categoryKey = sourceToCategory[baseSource] or "unknown"
        table.insert(categories[categoryKey].items, mat)
    end

    -- Sort within categories using centralized expansion order
    for _, category in pairs(categories) do
        table.sort(category.items, function(a, b)
            local aOrder = HDG_ExpansionData.GetSortOrder(a.expansion or "Unknown")
            local bOrder = HDG_ExpansionData.GetSortOrder(b.expansion or "Unknown")
            if aOrder ~= bOrder then return aOrder < bOrder end

            local aName = Data().GetCachedItemName(a.id) or ""
            local bName = Data().GetCachedItemName(b.id) or ""
            return aName < bName
        end)
    end

    return categories
end

-- Check if player can craft a specific recipe with current inventory
-- Returns: canCraft (bool), maxQuantity (number), missingMaterials (table or nil), materialPercentage (number 0-100)
function HDG_RecipesTab.CanCraftRecipe(recipe)
    if not recipe or not recipe.reagents or #recipe.reagents == 0 then
        return false, 0, nil, 0
    end

    local maxCraftable = 999999 -- Start with a very high number
    local missingMats = {}
    local canCraft = true

    -- Track material availability for quantity-weighted percentage calculation
    -- Formula: sum(min(have, need)) / sum(need) * 100
    -- This weights each material by how much is needed, not treating each type equally
    local totalQuantityNeeded = 0
    local totalQuantityHave = 0

    for _, reagent in ipairs(recipe.reagents) do
        -- Use GetItemCountWithVariants to count all quality ranks (R1+R2+R3)
        local have = Data().GetItemCountWithVariants(reagent.id)
        local need = reagent.amount or 1

        -- Accumulate for quantity-weighted percentage (cap have at need, don't count excess)
        totalQuantityNeeded = totalQuantityNeeded + need
        totalQuantityHave = totalQuantityHave + math.min(have, need)

        if have < need then
            -- Missing this material
            canCraft = false
            table.insert(missingMats, {
                id = reagent.id,
                have = have,
                need = need,
                missing = need - have
            })
        else
            -- Fully have this material
            -- Calculate how many times we could craft based on this material
            local possibleFromThisMat = math.floor(have / need)
            if possibleFromThisMat < maxCraftable then
                maxCraftable = possibleFromThisMat
            end
        end
    end

    -- Calculate quantity-weighted material percentage
    local materialPercentage = totalQuantityNeeded > 0 and (totalQuantityHave / totalQuantityNeeded * 100) or 0

    if not canCraft then
        return false, 0, missingMats, materialPercentage
    end

    return true, maxCraftable, nil, 100
end

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

local craftConfirmFrame = nil

-- 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_RecipesTab.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
        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 confirmScheme = GetScheme()
        if confirmScheme then
            craftConfirmFrame:SetBackdropColor(confirmScheme.bg.r, confirmScheme.bg.g, confirmScheme.bg.b, 0.95)
            craftConfirmFrame:SetBackdropBorderColor(confirmScheme.accent.r, confirmScheme.accent.g, confirmScheme.accent.b, 1)
        else
            craftConfirmFrame:SetBackdropColor(0.1, 0.1, 0.12, 0.95)
            craftConfirmFrame:SetBackdropBorderColor(0.08, 0.72, 0.65, 1)
        end
        RegisterWidget(craftConfirmFrame, "Frame")
        craftConfirmFrame:EnableMouse(true)
        craftConfirmFrame:SetMovable(true)
        craftConfirmFrame:RegisterForDrag("LeftButton")
        craftConfirmFrame:SetScript("OnDragStart", craftConfirmFrame.StartMoving)
        craftConfirmFrame:SetScript("OnDragStop", craftConfirmFrame.StopMovingOrSizing)

        -- Title
        local cfScheme = GetScheme()
        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

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

-- Get all craftable recipes for a profession (or all professions)
-- Returns table of { recipe = recipe, maxQuantity = number }
function HDG_RecipesTab.GetCraftableRecipes(professionFilter)
    local craftable = {}

    if not privateDB.professions then return craftable end

    for _, profession in ipairs(privateDB.professions) do
        -- Apply profession filter if specified
        if not professionFilter or professionFilter == "All" or profession.name == professionFilter then
            for _, recipe in ipairs(profession.items) do
                -- Only check recipes the player knows
                local knowsRecipe = Data().PlayerKnowsRecipeBySpellID(recipe.spell)

                if knowsRecipe then
                    local canCraft, maxQty = HDG_RecipesTab.CanCraftRecipe(recipe)
                    if canCraft and maxQty > 0 then
                        table.insert(craftable, {
                            recipe = recipe,
                            profession = profession.name,
                            maxQuantity = maxQty
                        })
                    end
                end
            end
        end
    end

    return craftable
end

-- Check if "What Can I Craft" mode is enabled
function HDG_RecipesTab.IsCraftableFilterEnabled()
    return GetConfig("craftableFilter") == true
end

-- Toggle "What Can I Craft" mode
function HDG_RecipesTab.SetCraftableFilter(enabled)
    Dispatch("SET_CONFIG", { key = "craftableFilter", value = enabled })
end

-- Recipe search filter (session-only, not persisted)
local recipeSearchFilter = ""

function HDG_RecipesTab.GetSearchFilter()
    return recipeSearchFilter
end

function HDG_RecipesTab.SetSearchFilter(text)
    recipeSearchFilter = (text or ""):lower()
end

-- Search reagents filter
function HDG_RecipesTab.IsSearchReagentsEnabled()
    return GetConfig("searchReagents") == true
end

function HDG_RecipesTab.SetSearchReagents(enabled)
    Dispatch("SET_CONFIG", { key = "searchReagents", value = enabled })
end

-- Uncollected only filter
function HDG_RecipesTab.IsUncollectedFilterEnabled()
    return GetConfig("uncollectedFilter") == true
end

function HDG_RecipesTab.SetUncollectedFilter(enabled)
    Dispatch("SET_CONFIG", { key = "uncollectedFilter", value = enabled })
end

-- Check if a recipe matches current filters
function HDG_RecipesTab.RecipeMatchesFilters(recipe)
    -- Search filter
    local search = recipeSearchFilter
    if search and search ~= "" then
        local recipeName = GetRecipeLowerName(recipe)
        local matchesName = recipeName:find(search, 1, true)

        -- Also search reagent names if enabled
        local matchesReagent = false
        if not matchesName and HDG_RecipesTab.IsSearchReagentsEnabled() and recipe.reagents then
            for _, reagent in ipairs(recipe.reagents) do
                -- Use reagent.name from DecorDB (faster, always available)
                local reagentName = reagent.name
                if reagentName then
                    reagentName = reagentName:lower()
                    if reagentName:find(search, 1, true) then
                        matchesReagent = true
                        break
                    end
                end
            end
        end

        if not matchesName and not matchesReagent then
            return false
        end
    end

    -- Uncollected filter
    if HDG_RecipesTab.IsUncollectedFilterEnabled() then
        local isCollected = Data().IsItemCollected(recipe.id)
        if isCollected then
            return false
        end
    end

    return true
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

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
            -- Find the first alt (not current character)
            for _, altInfo in ipairs(altsWhoKnow) do
                if not altInfo.isCurrent then
                    isAltKnown = true
                    altWhoKnows = altInfo.name -- Full name with realm (e.g., "CharName-Realm")
                    break
                end
            end
        end
    end

    local btn = CreateFrame("Button", nil, mainFrame.recipeContent, "BackdropTemplate")
    btn:SetSize(CONFIG.RECIPE_BUTTON_WIDTH, 18)
    btn:SetPoint("TOPLEFT", 5, yOffset)
    btn:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })

    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
        local profIcon = btn:CreateTexture(nil, "ARTWORK")
        profIcon:SetSize(14, 14)
        profIcon:SetPoint("LEFT", leftOffset, 0)
        HDG_ExpansionData.ApplyProfessionIcon(profIcon, recipe._professionName)
        -- Custom icons have proper transparency, no mask needed

        leftOffset = leftOffset + 16
    end

    local icon = btn:CreateTexture(nil, "OVERLAY")
    icon:SetSize(12, 12)
    icon:SetPoint("LEFT", leftOffset, 0)
    icon:SetTexture(isCollected and CONFIG.TEXTURES.CHECKMARK or CONFIG.TEXTURES.XMARK)
    leftOffset = leftOffset + 15

    local btnScheme = GetScheme()
    local nameText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    nameText:SetPoint("LEFT", leftOffset, 0)
    nameText:SetPoint("RIGHT", -55, 0)
    nameText:SetJustifyH("LEFT")
    nameText:SetWordWrap(false)
    ApplyFont(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
    nameText:SetText(itemName)
    nameText:SetTextColor(recipeColor.r, recipeColor.g, recipeColor.b, recipeColor.a)

    if showCraftableQty then
        local qtyText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        qtyText:SetPoint("RIGHT", -40, 0)
        ApplyFont(qtyText, btnScheme, "small")

        if recipe._maxCraftable and recipe._maxCraftable > 0 then
            local maxQty = recipe._maxCraftable
            if maxQty > 99 then
                qtyText:SetText(HDG.Theme.C_Success() .. "x99+|r")
            else
                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())
            qtyText:SetText(string.format("%s%.0f%%|r", color, pct))
        end
    end

    local expShort = HDG_ExpansionData.GetShortName(recipe.expansion)
    expColor = HDG_ExpansionData.GetColor(recipe.expansion)
    local expText = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    expText:SetPoint("RIGHT", -5, 0)
    expText:SetText(expShort)
    expText:SetTextColor(unpack(expColor))
    ApplyFont(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(" ")

        -- Show which alt knows this recipe
        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

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_RecipesTab.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 = HDG_RecipesTab.CanCraftRecipe(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

        local function sortByPercent(a, b)
            return (a._materialPercent or 0) > (b._materialPercent or 0)
        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

        local function renderTier(tier, label, color)
            if #tier > 0 then
                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)
                end
                yOffset = yOffset - 6
            end
        end

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

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

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 = CreateFrame("Frame", nil, recipeFrame, "BackdropTemplate")
    recipeHeaderBg:SetPoint("TOPLEFT", 1, -1)
    recipeHeaderBg:SetPoint("TOPRIGHT", -1, -1)
    recipeHeaderBg:SetHeight(22)
    recipeHeaderBg:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    recipeHeaderBg:SetBackdropColor(scheme.panel_header.r, scheme.panel_header.g, scheme.panel_header.b, scheme.panel_header.a)
    RegisterWidget(recipeHeaderBg, "PanelHeader")
    mainFrame.recipeHeaderBg = recipeHeaderBg

    local recipeTitle = recipeHeaderBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    recipeTitle:SetPoint("LEFT", 8, 0)
    ApplyFont(recipeTitle, scheme, "body")
    recipeTitle:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    recipeTitle:SetText("Professions")
    mainFrame.recipeTitle = recipeTitle
    HDG.Theme.StyleHeaderTitle(recipeTitle, recipeHeaderBg)

    -- 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)
    searchBox:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    local searchScheme = GetScheme()
    if searchScheme then
        searchBox:SetBackdropColor(searchScheme.bg.r, searchScheme.bg.g, searchScheme.bg.b, 1)
        searchBox:SetBackdropBorderColor(searchScheme.border.r, searchScheme.border.g, searchScheme.border.b, 0.8)
    else
        searchBox:SetBackdropColor(0.05, 0.05, 0.07, 1)
        searchBox:SetBackdropBorderColor(0.3, 0.3, 0.35, 0.8)
    end
    searchBox:SetAutoFocus(false)
    searchBox:SetTextInsets(6, 20, 0, 0)
    searchBox:SetMaxLetters(50)
    ApplyFont(searchBox, searchScheme, "small")

    -- Search placeholder text
    local searchPlaceholder = searchBox:CreateFontString(nil, "ARTWORK")
    searchPlaceholder:SetPoint("LEFT", 6, 0)
    ApplyFont(searchPlaceholder, searchScheme, "small")
    searchPlaceholder:SetText("Search recipes...")
    if searchScheme then
        searchPlaceholder:SetTextColor(searchScheme.text_dim.r, searchScheme.text_dim.g, searchScheme.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_RecipesTab.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_RecipesTab.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_RecipesTab.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, searchScheme, "small")

    uncollectedCheck:SetScript("OnClick", function(self)
        HDG_RecipesTab.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_RecipesTab.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, searchScheme, "small")

    searchReagentsCheck:SetScript("OnClick", function(self)
        HDG_RecipesTab.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)
    stickyHeader:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false, insets = { left = 0, right = 0, top = 0, bottom = 0 } })
    local stickyScheme = GetScheme()
    stickyHeader:SetBackdropColor(stickyScheme.panel.r, stickyScheme.panel.g, stickyScheme.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, stickyScheme, "small")
    stickyHeaderText:SetTextColor(stickyScheme.text.r, stickyScheme.text.g, stickyScheme.text.b, stickyScheme.text.a)
    mainFrame.stickyHeaderText = stickyHeaderText
    RegisterWidget(stickyHeaderText, "Text")

    local stickyHeaderPercentage = stickyHeader:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    stickyHeaderPercentage:SetPoint("RIGHT", -6, 0)
    ApplyFont(stickyHeaderPercentage, stickyScheme, "small")
    stickyHeaderPercentage:SetTextColor(stickyScheme.text.r, stickyScheme.text.g, stickyScheme.text.b, stickyScheme.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)
    profSectionHeader:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    local pshScheme = GetScheme()
    if pshScheme then
        profSectionHeader:SetBackdropColor(pshScheme.bg.r, pshScheme.bg.g, pshScheme.bg.b, 0.8)
        profSectionHeader:SetBackdropBorderColor(pshScheme.border.r, pshScheme.border.g, pshScheme.border.b, pshScheme.border.a)
    else
        profSectionHeader:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
        profSectionHeader:SetBackdropBorderColor(unpack(HDG_UIHelpers.BORDER_COLOR))
    end
    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, pshScheme, "body")
    profSectionText:SetTextColor(pshScheme.text.r, pshScheme.text.g, pshScheme.text.b, pshScheme.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, pshScheme, "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, pshScheme, "small")
    readyBtnText:SetText(L["BTN_READY"] or "Ready")

    local function UpdateCraftableToggle()
        local tglScheme = GetScheme()
        if HDG_Data.IsCraftableFilterEnabled() then
            allBtn:SetBackdropColor(0.05, 0.05, 0.05, 0)
            if tglScheme then
                allBtnText:SetTextColor(tglScheme.text_dim.r, tglScheme.text_dim.g, tglScheme.text_dim.b, tglScheme.text_dim.a)
                readyBtn:SetBackdropColor(tglScheme.button_active.r, tglScheme.button_active.g, tglScheme.button_active.b, 0.9)
                readyBtnText:SetTextColor(tglScheme.text.r, tglScheme.text.g, tglScheme.text.b, tglScheme.text.a)
            else
                allBtnText:SetTextColor(0.5, 0.5, 0.5, 1)
                readyBtn:SetBackdropColor(0.08, 0.45, 0.42, 0.9)
                readyBtnText:SetTextColor(1, 1, 1, 1)
            end
            allBtnLeftBorder:Show()
        else
            if tglScheme then
                allBtn:SetBackdropColor(tglScheme.button_active.r, tglScheme.button_active.g, tglScheme.button_active.b, 0.9)
                allBtnText:SetTextColor(tglScheme.text.r, tglScheme.text.g, tglScheme.text.b, tglScheme.text.a)
                readyBtnText:SetTextColor(tglScheme.text_dim.r, tglScheme.text_dim.g, tglScheme.text_dim.b, tglScheme.text_dim.a)
            else
                allBtn:SetBackdropColor(0.08, 0.45, 0.42, 0.9)
                allBtnText:SetTextColor(1, 1, 1, 1)
                readyBtnText:SetTextColor(0.5, 0.5, 0.5, 1)
            end
            readyBtn:SetBackdropColor(0.05, 0.05, 0.05, 0)
            allBtnLeftBorder:Hide()
        end
    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)
        local hvrScheme = GetScheme()
        if HDG_Data.IsCraftableFilterEnabled() then
            if hvrScheme then
                self:SetBackdropColor(hvrScheme.button_hover.r, hvrScheme.button_hover.g, hvrScheme.button_hover.b, 0.9)
            else
                self:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
            end
        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)
        local hvrScheme = GetScheme()
        if not HDG_Data.IsCraftableFilterEnabled() then
            if hvrScheme then
                self:SetBackdropColor(hvrScheme.button_hover.r, hvrScheme.button_hover.g, hvrScheme.button_hover.b, 0.9)
            else
                self:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
            end
        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 = CreateFrame("ScrollFrame", nil, recipeFrame, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 8, -284)
    scroll:SetPoint("BOTTOMRIGHT", -26, 8)

    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(CONFIG.RECIPE_BUTTON_WIDTH, 1)
    scroll:SetScrollChild(content)
    mainFrame.recipeContent = content
    mainFrame.recipeScroll = scroll

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

    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 shoppingHeaderBg = CreateFrame("Frame", nil, shoppingFrame, "BackdropTemplate")
    shoppingHeaderBg:SetPoint("TOPLEFT", 1, -1)
    shoppingHeaderBg:SetPoint("TOPRIGHT", -1, -1)
    shoppingHeaderBg:SetHeight(22)
    shoppingHeaderBg:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    shoppingHeaderBg:SetBackdropColor(scheme.panel_header.r, scheme.panel_header.g, scheme.panel_header.b, scheme.panel_header.a)
    RegisterWidget(shoppingHeaderBg, "PanelHeader")
    mainFrame.shoppingHeaderBg = shoppingHeaderBg

    local craftingQueueLabel = L["HEADER_CRAFTING_QUEUE"] or "Crafting Queue"
    local shoppingTitle = shoppingHeaderBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    shoppingTitle:SetPoint("CENTER", 0, 0)
    ApplyFont(shoppingTitle, scheme, "body")
    shoppingTitle:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    shoppingTitle:SetText(craftingQueueLabel)
    mainFrame.shoppingTitle = shoppingTitle
    mainFrame.craftingQueueLabel = craftingQueueLabel
    HDG.Theme.StyleHeaderTitle(shoppingTitle, shoppingHeaderBg)

    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 = CreateFrame("ScrollFrame", nil, shoppingFrame, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 8, -28)
    scroll:SetPoint("BOTTOMRIGHT", -26, lumberReservedHeight)
    mainFrame.shoppingScroll = scroll

    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(CONFIG.SHOPPING_ITEM_WIDTH, 1)
    scroll:SetScrollChild(content)
    mainFrame.shoppingContent = content

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

    -- 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)
    lumberFrame:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    local lumberScheme = GetScheme()
    lumberFrame:SetBackdropColor(lumberScheme.panel.r, lumberScheme.panel.g, lumberScheme.panel.b, 0.8)
    lumberFrame:SetBackdropBorderColor(lumberScheme.border.r, lumberScheme.border.g, lumberScheme.border.b, lumberScheme.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)
    pinBtn:SetBackdrop({
        bgFile = CONFIG.TEXTURES.WHITE_BG,
        edgeFile = CONFIG.TEXTURES.WHITE_BG,
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 }
    })
    pinBtn:SetBackdropColor(unpack(HDG_UIHelpers.BTN_NORMAL))
    pinBtn:SetBackdropBorderColor(unpack(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, lumberScheme, "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, lumberScheme, "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, lumberScheme, "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 materialsHeaderBg = CreateFrame("Frame", nil, materialsFrame, "BackdropTemplate")
    materialsHeaderBg:SetPoint("TOPLEFT", 1, -1)
    materialsHeaderBg:SetPoint("TOPRIGHT", -1, -1)
    materialsHeaderBg:SetHeight(22)
    materialsHeaderBg:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    materialsHeaderBg:SetBackdropColor(scheme.panel_header.r, scheme.panel_header.g, scheme.panel_header.b, scheme.panel_header.a)
    RegisterWidget(materialsHeaderBg, "PanelHeader")
    mainFrame.materialsHeaderBg = materialsHeaderBg

    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 materialsTitle = materialsHeaderBg:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    materialsTitle:SetPoint("LEFT", 8, 0)
    materialsTitle:SetPoint("RIGHT", -170, 0)  -- Leave space for buttons
    materialsTitle:SetJustifyH("LEFT")
    ApplyFont(materialsTitle, scheme, "body")
    materialsTitle:SetTextColor(scheme.text_header.r, scheme.text_header.g, scheme.text_header.b, scheme.text_header.a)
    materialsTitle:SetText(requiredMatsLabel)
    mainFrame.materialsTitle = materialsTitle
    HDG.Theme.StyleHeaderTitle(materialsTitle, materialsHeaderBg)

    -- 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
    })
    local sortScheme = GetScheme()
    sortModeBtn:SetBackdropColor(sortScheme.bg.r, sortScheme.bg.g, sortScheme.bg.b, 0.8)
    sortModeBtn:SetBackdropBorderColor(sortScheme.border.r, sortScheme.border.g, sortScheme.border.b, sortScheme.border.a)
    RegisterWidget(sortModeBtn, "Button")

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

    local function UpdateSortModeAppearance()
        local sortMode = GetConfig("materialSortMode") or "totals"
        local sScheme = GetScheme()
        if sortMode == "byRecipe" then
            sortModeBtn:SetBackdropColor(sScheme.button_hover.r, sScheme.button_hover.g, sScheme.button_hover.b, sScheme.button_hover.a)
            sortModeBtn:SetBackdropBorderColor(sScheme.accent.r, sScheme.accent.g, sScheme.accent.b, 0.8)
            sortModeText:SetText("By Recipe")
            sortModeText:SetTextColor(sScheme.text.r, sScheme.text.g, sScheme.text.b, sScheme.text.a)
        else
            sortModeBtn:SetBackdropColor(sScheme.panel.r, sScheme.panel.g, sScheme.panel.b, 0.8)
            sortModeBtn:SetBackdropBorderColor(sScheme.border.r, sScheme.border.g, sScheme.border.b, sScheme.border.a)
            sortModeText:SetText("Totals")
            sortModeText:SetTextColor(sScheme.text_dim.r, sScheme.text_dim.g, sScheme.text_dim.b, sScheme.text_dim.a)
        end
    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
    })
    local toggleScheme = GetScheme()
    toggleContainer:SetBackdropColor(toggleScheme.bg.r, toggleScheme.bg.g, toggleScheme.bg.b, 0.8)
    toggleContainer:SetBackdropBorderColor(toggleScheme.border.r, toggleScheme.border.g, toggleScheme.border.b, toggleScheme.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(toggleScheme.border.r, toggleScheme.border.g, toggleScheme.border.b, 0.5)

    local directText = directBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    directText:SetPoint("CENTER", 0, 0)
    ApplyFont(directText, toggleScheme, "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(toggleScheme.border.r, toggleScheme.border.g, toggleScheme.border.b, 0.5)

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

    -- Update toggle button appearance based on state
    local function UpdateToggleAppearance()
        local showRaw = GetConfig("showRawMaterials")
        local tScheme = GetScheme()
        if showRaw then
            if tScheme then
                directBtn:SetBackdropColor(tScheme.panel.r, tScheme.panel.g, tScheme.panel.b, 0.8)
                directBtn:SetBackdropBorderColor(tScheme.border.r, tScheme.border.g, tScheme.border.b, 0.5)
                rawBtn:SetBackdropColor(tScheme.button_hover.r, tScheme.button_hover.g, tScheme.button_hover.b, tScheme.button_hover.a)
                rawBtn:SetBackdropBorderColor(tScheme.accent.r, tScheme.accent.g, tScheme.accent.b, 0.8)
            else
                directBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
                rawBtn:SetBackdropColor(0.2, 0.2, 0.22, 1.0)
            end
            if tScheme then
                directText:SetTextColor(tScheme.text_dim.r, tScheme.text_dim.g, tScheme.text_dim.b, tScheme.text_dim.a)
                rawText:SetTextColor(tScheme.text.r, tScheme.text.g, tScheme.text.b, tScheme.text.a)
            else
                directText:SetTextColor(0.6, 0.6, 0.6, 1)
                rawText:SetTextColor(1, 1, 1, 1)
            end
            materialsTitle:SetText(rawMatsLabel)
        else
            if tScheme then
                directBtn:SetBackdropColor(tScheme.button_hover.r, tScheme.button_hover.g, tScheme.button_hover.b, tScheme.button_hover.a)
                directBtn:SetBackdropBorderColor(tScheme.accent.r, tScheme.accent.g, tScheme.accent.b, 0.8)
                rawBtn:SetBackdropColor(tScheme.panel.r, tScheme.panel.g, tScheme.panel.b, 0.8)
                rawBtn:SetBackdropBorderColor(tScheme.border.r, tScheme.border.g, tScheme.border.b, 0.5)
                directText:SetTextColor(tScheme.text.r, tScheme.text.g, tScheme.text.b, tScheme.text.a)
                rawText:SetTextColor(tScheme.text_dim.r, tScheme.text_dim.g, tScheme.text_dim.b, tScheme.text_dim.a)
            else
                directBtn:SetBackdropColor(0.2, 0.2, 0.22, 1.0)
                rawBtn:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
                directText:SetTextColor(1, 1, 1, 1)
                rawText:SetTextColor(0.6, 0.6, 0.6, 1)
            end
            materialsTitle:SetText(requiredMatsLabel)
        end
    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 = CreateFrame("ScrollFrame", nil, materialsFrame, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 8, -28)
    scroll:SetPoint("BOTTOMRIGHT", -26, 68)

    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(CONFIG.MATERIAL_ITEM_WIDTH, 1)
    scroll:SetScrollChild(content)
    mainFrame.materialsContent = content
    mainFrame.materialsScroll = scroll

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

    -- 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)
    addAllBar:SetBackdrop({ bgFile = CONFIG.TEXTURES.WHITE_BG, tile = false })
    local addAllScheme = GetScheme()
    if addAllScheme then
        addAllBar:SetBackdropColor(addAllScheme.button_active.r * 0.3, addAllScheme.button_active.g * 0.3, addAllScheme.button_active.b * 0.3, 0.3)
    else
        addAllBar:SetBackdropColor(0.08, 0.23, 0.23, 0.3)
    end

    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, addAllScheme, "body")
    buttonText:SetText(addAllLabel)
    buttonText:SetShadowOffset(0, 0)
    if addAllScheme then
        buttonText:SetTextColor(addAllScheme.text.r, addAllScheme.text.g, addAllScheme.text.b, addAllScheme.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

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

    -- Update title with item count
    local shoppingList = GetShoppingList()
    if mainFrame.shoppingTitle and mainFrame.craftingQueueLabel then
        local count = #shoppingList
        local countScheme = GetScheme()
        if count > 0 then
            -- Use accent color for count badge
            local accentCode = string.format("|cFF%02x%02x%02x",
                math.floor(countScheme.accent.r * 255),
                math.floor(countScheme.accent.g * 255),
                math.floor(countScheme.accent.b * 255))
            mainFrame.shoppingTitle:SetText(mainFrame.craftingQueueLabel .. " " .. accentCode .. "(" .. 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

    -- Group items by primary crafter
    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)

    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)

        local hfScheme = GetScheme()
        ApplyFont(headerFrame.text, hfScheme, "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
            local item = itemData.item
            local i = itemData.index

            -- Check if this item has subsidiary recipes (crafted sub-components)
            local hasSubsidiaries = 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_RecipesTab.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
                local expScheme = GetScheme()
                frame.expandIcon:Show()
                ApplyFont(frame.expandIcon, expScheme, "small")
                frame.expandIcon:SetText(isExpanded and (HDG.Theme.C_Warning() .. "-|r") or (HDG.Theme.C_Warning() .. "+|r"))
                if expScheme then
                    frame.expandIcon:SetTextColor(expScheme.warning.r, expScheme.warning.g, expScheme.warning.b, expScheme.warning.a)
                end
            end

            -- Profession icon (fallback to DB lookup for legacy items without profession field)
            local profession = item.profession
            if not profession and item.id and HDG_DecorDB then
                for _, recipe in pairs(HDG_DecorDB) do
                    if recipe.itemID == item.id then
                        profession = recipe.profession
                        break
                    end
                end
            end
            frame.profIcon:ClearAllPoints()
            frame.profIcon:SetPoint("LEFT", 4 + EXPAND_ICON_WIDTH, 0)
            HDG_ExpansionData.ApplyProfessionIcon(frame.profIcon, profession)

            local nameScheme = GetScheme()
            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, nameScheme, "small")
            if nameScheme then
                frame.nameText:SetTextColor(nameScheme.text.r, nameScheme.text.g, nameScheme.text.b, nameScheme.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)
                local craftScheme = GetScheme()
                if craftScheme then
                    craftBtn:SetBackdropColor(craftScheme.success.r, craftScheme.success.g, craftScheme.success.b, 1)
                    craftBtn:SetBackdropBorderColor(craftScheme.success.r * 0.7, craftScheme.success.g * 0.7, craftScheme.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()
                local btnScheme = GetScheme()
                ApplyFont(btnText, btnScheme, "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 = 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
                    local subScheme = GetScheme()
                    ApplyFont(subFrame.nameText, subScheme, "small")
                    if subScheme then
                        subFrame.nameText:SetTextColor(subScheme.text_dim.r, subScheme.text_dim.g, subScheme.text_dim.b, subScheme.text_dim.a)
                    end
                    subFrame.nameText:SetText(sub.name or ("Item " .. sub.itemID))

                    -- Quantity
                    ApplyFont(subFrame.qtyText, subScheme, "small")
                    subFrame.qtyText:SetText("x" .. sub.qty)
                    if subScheme then
                        subFrame.qtyText:SetTextColor(subScheme.warning.r, subScheme.warning.g, subScheme.warning.b, subScheme.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
        end -- end items loop
    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_RecipesTab.AggregateMaterialsByRecipeRaw(shoppingList)
    else
        recipes = HDG_RecipesTab.AggregateMaterialsByRecipe(shoppingList)
    end
    local yOffset = 0
    local collapsedMaterials = GetUIState("collapsedMaterials") or {}
    local categoryOrder = { "vendor", "gather", "craft", "unknown" }

    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)
        local rScheme = GetScheme()
        recipeHeader:SetBackdropColor(rScheme.accent.r * 0.3, rScheme.accent.g * 0.3, rScheme.accent.b * 0.3, 0.9)

        ApplyFont(recipeHeader.collapseIcon, rScheme, "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, rScheme, "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, rScheme, "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(rScheme.accent.r * 0.5, rScheme.accent.g * 0.5, rScheme.accent.b * 0.5, 1.0)
        end)
        recipeHeader:SetScript("OnLeave", function(self)
            self:SetBackdropColor(rScheme.accent.r * 0.3, rScheme.accent.g * 0.3, rScheme.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_RecipesTab.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(rScheme.panel.r, rScheme.panel.g, rScheme.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, rScheme, "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(rScheme.panel.r, rScheme.panel.g, rScheme.panel.b, 0.4)
                        end

                        local matName = Data().GetCachedItemName(mat.id)
                        row.nameText:SetPoint("LEFT", 4, 0)
                        row.nameText:SetWidth(130)
                        ApplyFont(row.nameText, rScheme, "small")
                        row.nameText:SetText(matName)
                        row.nameText:SetTextColor(rScheme.text.r, rScheme.text.g, rScheme.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, rScheme, "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, GetScheme(), "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
    elseif mainFrame.materialsCostText then
        -- Calculate estimated cost for By Recipe mode
        local hasPriceAddon, priceAddonName = Data().HasPriceAddon()
        if hasPriceAddon then
            local totalCost = 0
            local missingPrices = 0
            for _, recipeEntry in ipairs(recipes) do
                for _, mat in pairs(recipeEntry.materials) do
                    local matPrice = Data().GetItemPrice(mat.id)
                    if matPrice then
                        local stillNeeded = math.max(0, mat.needed - mat.have)
                        totalCost = totalCost + (matPrice * stillNeeded)
                    elseif mat.have < mat.needed then
                        missingPrices = missingPrices + 1
                    end
                end
            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
        else
            mainFrame.materialsCostText:SetText(HDG.Theme.C_Dim() .. "Install TSM/Auctionator for price data|r")
        end
    end

    mainFrame.materialsContent:SetHeight(math.abs(yOffset) + 20)
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

    -- 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_RecipesTab.AggregateRawMaterials(shoppingList)
    else
        -- Direct mode: show materials as listed in recipes
        materials = HDG_RecipesTab.AggregateMaterials(shoppingList)
        intermediates = nil
    end
    local sortedMaterials = HDG_RecipesTab.FilterAndSortMaterials(materials)
    local categorized = HDG_RecipesTab.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
            local isCollapsed = collapsedMaterials[catKey]

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

            ApplyFont(header.collapseIcon, headerScheme, "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, headerScheme, "small")
            header.headerText:SetText(category.name)
            header.headerText:SetTextColor(headerScheme.text_header.r, headerScheme.text_header.g, headerScheme.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(headerScheme.button_hover.r, headerScheme.button_hover.g, headerScheme.button_hover.b, 1.0) end)
            header:SetScript("OnLeave", function(self) self:SetBackdropColor(headerScheme.panel.r, headerScheme.panel.g, headerScheme.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 matScheme = GetScheme()
                    local expColor = HDG_ExpansionData.GetColor(mat.expansion)
                    if expColor then
                        row:SetBackdropColor(expColor[1], expColor[2], expColor[3], 0.15)
                    else
                        row:SetBackdropColor(matScheme.panel.r, matScheme.panel.g, matScheme.panel.b, 0.5)
                    end

                    local matName = Data().GetCachedItemName(mat.id)
                    row.nameText:SetPoint("LEFT", 4, 0)
                    row.nameText:SetWidth(150)
                    ApplyFont(row.nameText, matScheme, "small")
                    row.nameText:SetText(matName)
                    row.nameText:SetTextColor(matScheme.text.r, matScheme.text.g, matScheme.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, matScheme, "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
        end
    end

    -- Show intermediates section in Raw mode (crafted sub-components)
    if intermediates and GetConfig("showRawMaterials") then
        -- 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)
            local intHdrScheme = GetScheme()
            if intHdrScheme then
                header:SetBackdropColor(intHdrScheme.button_active.r * 0.5, intHdrScheme.button_active.g * 0.6, intHdrScheme.button_active.b * 0.6, 0.9)
            else
                header:SetBackdropColor(0.15, 0.25, 0.25, 0.9)
            end

            ApplyFont(header.collapseIcon, intHdrScheme, "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, intHdrScheme, "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)
                local hvrScheme = GetScheme()
                if hvrScheme then
                    self:SetBackdropColor(hvrScheme.button_hover.r, hvrScheme.button_hover.g, hvrScheme.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)
                local lvScheme = GetScheme()
                if lvScheme then
                    self:SetBackdropColor(lvScheme.button_active.r * 0.5, lvScheme.button_active.g * 0.6, lvScheme.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)
                    local rowScheme = GetScheme()
                    if rowScheme then
                        row:SetBackdropColor(rowScheme.panel.r, rowScheme.panel.g * 1.2, rowScheme.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, rowScheme, "small")
                    row.nameText:SetText(interName)
                    if rowScheme then
                        row.nameText:SetTextColor(rowScheme.text.r, rowScheme.text.g, rowScheme.text.b, rowScheme.text.a)
                    end

                    local countStr = string.format("x%d", inter.needed)
                    row.countText:SetPoint("RIGHT", -10, 0)
                    ApplyFont(row.countText, rowScheme, "small")
                    row.countText:SetText(countStr)
                    if rowScheme then
                        row.countText:SetTextColor(rowScheme.accent.r, rowScheme.accent.g, rowScheme.accent.b, rowScheme.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 childScheme = GetScheme()
                                local childName = childData.name or ("Item #" .. childID)
                                childRow.nameText:SetPoint("LEFT", 8, 0)
                                childRow.nameText:SetWidth(140)
                                ApplyFont(childRow.nameText, childScheme, "small")
                                childRow.nameText:SetText("- " .. childName)
                                if childScheme then
                                    childRow.nameText:SetTextColor(childScheme.text_dim.r, childScheme.text_dim.g, childScheme.text_dim.b, childScheme.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, childScheme, "small")
                                childRow.countText:SetText("x" .. totalChildNeeded)
                                if childScheme then
                                    childRow.countText:SetTextColor(childScheme.text_dim.r * 0.8, childScheme.text_dim.g * 0.8, childScheme.text_dim.b * 0.8, childScheme.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
    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, GetScheme(), "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
        if mainFrame.materialsCostText then
            local hasPriceAddon, priceAddonName = Data().HasPriceAddon()
            if hasPriceAddon then
                local totalCost = 0
                local missingPrices = 0
                for _, catKey in ipairs(categoryOrder) do
                    local category = categorized[catKey]
                    if category and #category.items > 0 then
                        for _, mat in ipairs(category.items) do
                            if mat.estimatedCost then
                                totalCost = totalCost + mat.estimatedCost
                            elseif mat.have < mat.needed then
                                missingPrices = missingPrices + 1
                            end
                        end
                    end
                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
            else
                mainFrame.materialsCostText:SetText(HDG.Theme.C_Dim() .. "Install TSM/Auctionator for price data|r")
            end
        end
    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_RecipesTab.AggregateRawMaterials(shoppingList)
        modeLabel = "raw"
    else
        -- Direct mode: get materials as listed in recipes
        materials = HDG_RecipesTab.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_RecipesTab.FilterAndSortMaterials(materials)
    local categorized = HDG_RecipesTab.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
-- ============================================================================

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

    local children = { mainFrame.professionTabs:GetChildren() }
    for _, child in ipairs(children) do child:Hide() end

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

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

    -- Get the per-character profession filter
    local currentFilter = HDG_RecipesTab.GetProfessionFilter()

    local yOffset = 0

    -- "All Professions" row at the top
    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 })

    local isAllSelected = (currentFilter == "All")
    local filterScheme = GetScheme()
    if isAllSelected then
        allBtn:SetBackdropColor(filterScheme.accent.r, filterScheme.accent.g, filterScheme.accent.b, 0.3)
    else
        allBtn:SetBackdropColor(filterScheme.button_normal.r, filterScheme.button_normal.g, filterScheme.button_normal.b, 0.2)
    end

    local allHighlight = allBtn:CreateTexture(nil, "HIGHLIGHT")
    allHighlight:SetAllPoints()
    allHighlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
    allHighlight:SetBlendMode("ADD")
    if isAllSelected then
        allHighlight:SetVertexColor(1, 1, 1, 0.5); allHighlight:Show()
    else allHighlight:Hide() end

    local allProfsLabel = L["FILTER_ALL_PROFESSIONS"] or "All Professions"
    local allText = allBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    allText:SetPoint("LEFT", 6, 0)
    allText:SetText(isAllSelected and (HDG.Theme.C_Warning() .. allProfsLabel .. "|r") or (HDG.Theme.C_Text() .. allProfsLabel .. "|r"))
    ApplyFont(allText, filterScheme, "small")

    allBtn:SetScript("OnClick", function()
        HDG_RecipesTab.SetProfessionFilter("All")
        HDG_UI.RefreshUI()
    end)

    allBtn:SetScript("OnEnter", function(self)
        if currentFilter ~= "All" then
            self:SetBackdropColor(filterScheme.button_hover.r, filterScheme.button_hover.g, filterScheme.button_hover.b, 0.4)
        end
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:AddLine(allProfsLabel, 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)
        if currentFilter == "All" then
            self:SetBackdropColor(filterScheme.accent.r, filterScheme.accent.g, filterScheme.accent.b, 0.3)
        else
            self:SetBackdropColor(filterScheme.button_normal.r, filterScheme.button_normal.g, filterScheme.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)
    yOffset = yOffset - 6

    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 })

        local localizedProfName = HDG_ExpansionData.GetLocalizedProfessionName(profName)
        local hasProf = Data().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 Data().PlayerKnowsRecipeBySpellID and Data().PlayerKnowsRecipeBySpellID(recipe.spell) then
                        recipesKnown = recipesKnown + 1
                    end
                    if Data().IsItemCollected(recipe.id) then
                        collectedCount = collectedCount + 1
                    end
                end
                break
            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)

        -- Use scheme colors (filterScheme defined above)
        if isSelected then
            btn:SetBackdropColor(filterScheme.accent.r, filterScheme.accent.g, filterScheme.accent.b, 0.3)
        else
            btn:SetBackdropColor(filterScheme.button_normal.r, filterScheme.button_normal.g, filterScheme.button_normal.b, 0.2)
        end

        local highlight = btn:CreateTexture(nil, "HIGHLIGHT")
        highlight:SetAllPoints()
        highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
        highlight:SetBlendMode("ADD")
        if isSelected then
            highlight:SetVertexColor(1, 1, 1, 0.5); highlight:Show()
        else highlight:Hide() end

        local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        text:SetPoint("LEFT", 6, 0)
        ApplyFont(text, filterScheme, "small")
        text:SetText(localizedProfName)
        btn.textLabel = text -- store reference for coloring after altKnown is determined

        local collectionBadge = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        collectionBadge:SetPoint("RIGHT", COLLECTION_COL_RIGHT, 0)
        collectionBadge:SetWidth(COL_WIDTH)
        collectionBadge:SetJustifyH("LEFT")
        ApplyFont(collectionBadge, filterScheme, "small")
        local collectionColor = collectionPercentage >= 100 and HDG.Theme.C_Success() or HDG.Theme.C_Warning()
        collectionBadge:SetText(string.format("%s%d/%d|r", collectionColor, collectedCount, totalCount))

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

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

        -- Apply profession name color based on known status (uses recipe scheme colors)
        local profColor
        if isSelected or hasProf then
            profColor = filterScheme.text_recipe_known or {r=0.80, g=0.29, b=0.09, a=1}
        elseif altKnown > 0 then
            profColor = filterScheme.text_recipe_alt or {r=1, g=1, b=1, a=1}
        else
            profColor = filterScheme.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.professionName = profName             -- Internal English name for filtering
        btn.localizedProfName = localizedProfName -- Localized name for display
        btn.hasProf = hasProf
        btn.recipesKnown = recipesKnown
        btn.collectedCount = collectedCount
        btn.totalCount = totalCount
        btn.recipesPercentage = recipesPercentage
        btn.collectionPercentage = collectionPercentage

        btn:SetScript("OnClick", function(self)
            HDG_RecipesTab.SetProfessionFilter(self.professionName)
            HDG_UI.RefreshUI()
        end)

        btn:SetScript("OnEnter", function(self)
            if currentFilter ~= self.professionName then
                self:SetBackdropColor(filterScheme.button_hover.r, filterScheme.button_hover.g, filterScheme.button_hover.b, 0.4)
            end
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:AddLine(self.localizedProfName, 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, self.totalCount,
                    self.recipesPercentage), 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, self.totalCount,
                self.collectionPercentage), 1, 1, 1)
            GameTooltip:Show()
        end)

        btn:SetScript("OnLeave", function(self)
            if currentFilter == self.professionName then
                self:SetBackdropColor(filterScheme.accent.r, filterScheme.accent.g, filterScheme.accent.b, 0.3)
            else
                self:SetBackdropColor(filterScheme.button_normal.r, filterScheme.button_normal.g, filterScheme.button_normal.b, 0.2)
            end
            GameTooltip:Hide()
        end)

        yOffset = yOffset - 20
    end

    -- Calculate totals for header tooltips (no visible TOTAL row)
    local grandTotalRecipes, grandKnownRecipes, grandTotalDecor, grandCollectedDecor = 0, 0, 0, 0
    for _, profData in ipairs(privateDB.professions or {}) do
        local hasProf = Data().PlayerHasProfession(profData.name)
        local profKnownRecipes = 0
        if profData.items then
            for _, recipe in ipairs(profData.items) do
                grandTotalDecor = grandTotalDecor + 1
                grandTotalRecipes = grandTotalRecipes + 1
                if hasProf and Data().PlayerKnowsRecipeBySpellID and Data().PlayerKnowsRecipeBySpellID(recipe.spell) then
                    profKnownRecipes = profKnownRecipes + 1
                end
                if Data().IsItemCollected(recipe.id) then grandCollectedDecor = grandCollectedDecor + 1 end
            end
        end
        if hasProf then
            grandKnownRecipes = grandKnownRecipes + profKnownRecipes
        else
            local altKnown = Data().GetBestAltRecipeCount(profData.name)
            if altKnown > 0 then grandKnownRecipes = grandKnownRecipes + altKnown end
        end
    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
        local allProfsLabel = L["FILTER_ALL_PROFESSIONS"] or "All Professions"
        if HDG_RecipesTab.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

    ClearScrollContent(mainFrame.recipeContent)
    HideProfessionSectionHeader()
    self:RefreshProfessionTabs()

    local yOffset = 0
    local playerProfessions = Data().GetPlayerProfessions()
    local isAllMode = (HDG_RecipesTab.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_RecipesTab.RecipeMatchesFilters(recipe) then
                    local isKnown = HDG_Data.PlayerKnowsRecipeBySpellID(recipe.spell)
                    local canCraft, maxQty, _, matPercent = HDG_RecipesTab.CanCraftRecipe(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

        local function sortByPercent(a, b)
            return (a._materialPercent or 0) > (b._materialPercent or 0)
        end
        table.sort(tier75to99, sortByPercent)
        table.sort(tier50to74, sortByPercent)
        table.sort(tierBelow50, sortByPercent)

        -- Render unified tiers
        local function renderTier(tier, label, color)
            if #tier > 0 then
                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, true) -- showProfessionIcon = true
                end
                yOffset = yOffset - 6
            end
        end

        renderTier(tier100, "Ready to Craft", HDG.Theme.C_Success())
        renderTier(tier75to99, "Almost Ready (75-99%)", HDG.Theme.C_Success())
        renderTier(tier50to74, "Halfway There (50-74%)", HDG.Theme.C_Warning())
        renderTier(tierBelow50, "Need Materials (<50%)", HDG.Theme.C_Error())
    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()
    local HDG = _G.HousingDecorGuide
    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

-- Register handlers after a short delay to ensure EventBus is ready
C_Timer.After(0.1, RegisterEventBusHandlers)

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

_G.HDG_RecipesTab = HDG_RecipesTab

return HDG_RecipesTab
