-- ============================================================================
-- HDG_PowerCrafting.lua - DAG-Based Recursive Recipe Resolution
-- ============================================================================
-- Handles recursive crafting dependency resolution:
-- - Builds dependency graphs for crafted reagents
-- - Detects which sub-components player can craft
-- - Calculates total raw materials needed
-- - Generates crafting order (leaf nodes first via topological sort)
-- - Handles circular dependencies gracefully
-- ============================================================================

local addonName, privateDB = ...

HDG_PowerCrafting = {}

-- Multi-craft tracking. RecipesTab sets itemID + remaining when initiating a
-- craft from the addon UI; RegisterCraftAutoDecrement decrements remaining
-- on each TRADE_SKILL_ITEM_CRAFTED_RESULT and clears itemID + closes the
-- profession window when the batch completes. Lives on privateDB so both
-- modules can reach it without a circular import.
privateDB.pendingCraftInfo = {
    itemID    = nil,
    remaining = 0,
}

-- Apply Blizzard's recipe-name filter on the open profession window so only
-- House Decor recipes show. Cheap "find this fast" helper for crafters who
-- don't want to scroll. Used by /hdg decor.
function HDG_PowerCrafting.FilterProfessionWindowToHouseDecor()
    if not C_TradeSkillUI then
        print("|cFFFF0000[Housing Decor Guide]|r Trade skill API not available.")
        return
    end
    if not C_TradeSkillUI.IsTradeSkillReady() then
        print("|cFFfcd34d[Housing Decor Guide]|r Open a profession window first, then run |cFFFFFFFF/hdg decor|r")
        return
    end
    if C_TradeSkillUI.SetRecipeItemNameFilter then
        C_TradeSkillUI.SetRecipeItemNameFilter("House Decor")
        print("|cFF14b8a6[Housing Decor Guide]|r Filtered to 'House Decor' recipes. Clear the search box to reset.")
    else
        print("|cFFFF0000[Housing Decor Guide]|r SetRecipeItemNameFilter not available in this WoW version.")
    end
end

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

local MAX_RECURSION_DEPTH = 10 -- Prevent infinite loops
local CRAFTED_SOURCE = "Crafted"
local dependencyGraphCache = {} -- reagent signature → cached graph result

-- ============================================================================
-- ITEM ID LOOKUP INDEX (built lazily)
-- HDG_ProfessionsDB is keyed by ITEMID (the produced item's id). The actual
-- Blizzard recipe spell id lives on each entry's `.recipeID` field. The
-- legacy variable name `recipeID` in this module historically referred to
-- the table key (= itemID); to look up "does the player know how to craft
-- this", you must read `entry.recipeID` and pass THAT to
-- HDG_Data.PlayerKnowsRecipeBySpellID.
-- ============================================================================

-- itemIDIndex[itemID] = ProfessionsDB key for the preferred recipe (note:
-- the key happens to equal the produced itemID, since the DB is itemID-keyed).
local itemIDIndex = nil
-- allRecipesIndex[itemID] = list of ProfessionsDB keys producing that item.
local allRecipesIndex = nil
local PruneProfessionsDB    -- forward declaration

local function BuildItemIDIndex()
    if itemIDIndex then return end
    itemIDIndex = {}
    allRecipesIndex = {}

    if not HDG_ProfessionsDB then return end

    -- First pass: collect all recipes for each itemID
    for recipeID, entry in pairs(HDG_ProfessionsDB) do
        if entry.itemID then
            if not allRecipesIndex[entry.itemID] then
                allRecipesIndex[entry.itemID] = {}
            end
            table.insert(allRecipesIndex[entry.itemID], recipeID)
        end
    end

    -- Second pass: select preferred recipe for each itemID
    -- Priority: Mining > other professions > Alchemy Transmutes
    for itemID, recipeIDs in pairs(allRecipesIndex) do
        local preferredRecipeID = nil
        local preferredPriority = 0 -- Higher = better

        for _, recipeID in ipairs(recipeIDs) do
            local entry = HDG_ProfessionsDB[recipeID]
            local priority = 1 -- Default

            if entry.profession == "Mining" then
                priority = 10 -- Highest priority for smelting
            elseif entry.profession == "Alchemy" and entry.name and entry.name:match("^Transmute:") then
                priority = 0  -- Lowest priority for transmutes
            elseif entry.profession ~= "Alchemy" then
                priority = 5  -- Medium priority for other professions
            end

            if priority > preferredPriority then
                preferredPriority = priority
                preferredRecipeID = recipeID
            end
        end

        itemIDIndex[itemID] = preferredRecipeID
    end

    PruneProfessionsDB()
end

-- ============================================================================
-- PRUNE: keep only housing-relevant recipes to free ~6-12 MB
-- ============================================================================

PruneProfessionsDB = function()
    if not HDG_ProfessionsDB then return end

    local needed = {} -- [recipeID] = true

    -- Category A: housing recipes from DecorDB. HDG_ProfessionsDB is keyed
    -- by itemID, NOT spellID, so we look up by recipe.itemID. `needed` is
    -- consumed at line 124 with the same key space (ProfessionsDB key =
    -- itemID), so we record itemIDs here to match cats B + C.
    if HDG_DecorDB then
        for _, recipe in pairs(HDG_DecorDB) do
            if recipe.itemID and HDG_ProfessionsDB[recipe.itemID] then
                needed[recipe.itemID] = true
            end
        end
    end

    -- Category B: recipes that produce items marked "Crafted" in ReagentsDB (sub-recipe DAG)
    if HDG_ReagentsDB and itemIDIndex then
        for reagentID, entry in pairs(HDG_ReagentsDB) do
            local source = entry[1]
            if source == "Crafted" or (source and source:match("^Crafted")) then
                -- Find all recipes that produce this crafted reagent
                if allRecipesIndex and allRecipesIndex[reagentID] then
                    for _, recipeID in ipairs(allRecipesIndex[reagentID]) do
                        needed[recipeID] = true
                    end
                end
            end
        end
    end

    -- Category C: recipes with non-empty slot variants (quality R1/R2/R3)
    for recipeID, entry in pairs(HDG_ProfessionsDB) do
        if entry.slots then
            for _, slot in ipairs(entry.slots) do
                if slot.variants and #slot.variants > 0 then
                    needed[recipeID] = true
                    break
                end
            end
        end
    end

    -- Build filtered table
    local filtered = {}
    for recipeID in pairs(needed) do
        filtered[recipeID] = HDG_ProfessionsDB[recipeID]
    end
    _G.HDG_ProfessionsDB = filtered

    -- Rebuild indexes from filtered data
    itemIDIndex = {}
    allRecipesIndex = {}
    for recipeID, entry in pairs(filtered) do
        if entry.itemID then
            if not allRecipesIndex[entry.itemID] then
                allRecipesIndex[entry.itemID] = {}
            end
            table.insert(allRecipesIndex[entry.itemID], recipeID)
        end
    end
    for itemID, recipeIDs in pairs(allRecipesIndex) do
        local preferredRecipeID, preferredPriority = nil, 0
        for _, recipeID in ipairs(recipeIDs) do
            local entry = filtered[recipeID]
            local priority = 1
            if entry.profession == "Mining" then
                priority = 10
            elseif entry.profession == "Alchemy" and entry.name and entry.name:match("^Transmute:") then
                priority = 0
            elseif entry.profession ~= "Alchemy" then
                priority = 5
            end
            if priority > preferredPriority then
                preferredPriority = priority
                preferredRecipeID = recipeID
            end
        end
        itemIDIndex[itemID] = preferredRecipeID
    end
end

-- ============================================================================
-- REVERSE REAGENT INDEX: reagentItemID → recipes that use it
-- ============================================================================

local reagentToRecipesIndex = nil -- [reagentItemID] = { recipe1, recipe2, ... }

local function BuildReagentIndex()
    if reagentToRecipesIndex then return end
    reagentToRecipesIndex = {}
    if not HDG_DecorDB then return end

    for recipeKey, recipe in pairs(HDG_DecorDB) do
        if recipe.reagents then
            -- Build reagentsArray once per recipe (shared across all reagent entries)
            local reagentsArray = {}
            for rID, rData in pairs(recipe.reagents) do
                reagentsArray[#reagentsArray + 1] = { id = rID, name = rData.name, qty = rData.qty or 1 }
            end
            local recipeEntry = {
                id = recipe.itemID or recipeKey,
                name = recipe.name,
                spell = recipe.spellID,
                expansion = recipe.expansion,
                profession = recipe.profession,
                reagents = reagentsArray,
            }
            for reagentID in pairs(recipe.reagents) do
                if not reagentToRecipesIndex[reagentID] then
                    reagentToRecipesIndex[reagentID] = {}
                end
                reagentToRecipesIndex[reagentID][#reagentToRecipesIndex[reagentID] + 1] = recipeEntry
            end
        end
    end

    -- Sort each reagent's recipe list by name
    for _, recipes in pairs(reagentToRecipesIndex) do
        table.sort(recipes, function(a, b) return (a.name or "") < (b.name or "") end)
    end
end

function HDG_PowerCrafting.GetRecipesForReagent(reagentItemID)
    BuildReagentIndex()
    return reagentToRecipesIndex and reagentToRecipesIndex[reagentItemID] or {}
end

function HDG_PowerCrafting.GetAllHousingReagentIDs()
    BuildReagentIndex()
    if not reagentToRecipesIndex then return {} end
    local ids = {}
    for reagentID in pairs(reagentToRecipesIndex) do
        ids[#ids + 1] = reagentID
    end
    return ids
end

function HDG_PowerCrafting.GetCraftReadiness(recipe)
    if not recipe or not recipe.reagents then return { owned = 0, total = 0, ready = false } end
    local owned, total = 0, 0
    for _, reagent in ipairs(recipe.reagents) do
        local need = reagent.qty or 1
        total = total + 1
        local have = HDG_Data and HDG_Data.GetItemCount(reagent.id) or 0
        if have >= need then owned = owned + 1 end
    end
    return { owned = owned, total = total, ready = (owned == total and total > 0) }
end

-- Get recipe entry by output itemID (returns preferred recipe)
local function GetRecipeByItemID(itemID)
    BuildItemIDIndex()
    local recipeID = itemIDIndex and itemIDIndex[itemID]
    if recipeID then
        return HDG_ProfessionsDB[recipeID]
    end
    return nil
end

-- Public version for other modules
function HDG_PowerCrafting.GetRecipeByItemID(itemID)
    return GetRecipeByItemID(itemID)
end

-- Get ALL recipes that produce a given itemID (for showing alternatives)
function HDG_PowerCrafting.GetAllRecipesForItem(itemID)
    BuildItemIDIndex()
    if not allRecipesIndex or not allRecipesIndex[itemID] then
        return {}
    end

    -- Note: `dbKey` is the ProfessionsDB table key (= produced itemID, since
    -- the DB is itemID-keyed). The real Blizzard recipe spell id is on
    -- `entry.recipeID`. We expose BOTH so callers don't have to dereference
    -- back into the DB just to learn the spell id.
    local recipes = {}
    for _, dbKey in ipairs(allRecipesIndex[itemID]) do
        local entry = HDG_ProfessionsDB[dbKey]
        if entry then
            table.insert(recipes, {
                dbKey = dbKey,
                recipeSpellID = entry.recipeID,
                name = entry.name,
                profession = entry.profession,
                isPreferred = (itemIDIndex and dbKey == itemIDIndex[itemID]),
                slots = entry.slots,
            })
        end
    end

    -- Sort: preferred first, then by profession
    table.sort(recipes, function(a, b)
        if a.isPreferred ~= b.isPreferred then
            return a.isPreferred
        end
        return (a.profession or "") < (b.profession or "")
    end)

    return recipes
end

-- ============================================================================
-- DEPENDENCY GRAPH BUILDER
-- ============================================================================

-- Generate deterministic cache key from recipe reagents
local function GetReagentSignature(decorRecipe)
    if not decorRecipe or not decorRecipe.reagents then return nil end
    local parts = {}
    for reagentID, reagentData in pairs(decorRecipe.reagents) do
        parts[#parts + 1] = reagentID .. ":" .. (reagentData.qty or 1)
    end
    table.sort(parts)
    return table.concat(parts, ",")
end

-- Check if an item is a crafted reagent
function HDG_PowerCrafting.IsCraftedReagent(itemID)
    if not itemID then return false end
    local entry = HDG_ReagentsDB and HDG_ReagentsDB[itemID]
    local source = entry and entry[1]
    if source then
        return source == CRAFTED_SOURCE or source:match("^Crafted")
    end
    return false
end

-- Get the recipe for a crafted item (if known)
-- Uses HDG_ProfessionsDB v2 (slot-based format, keyed by recipeID)
-- Only returns "basic" slot reagents (required materials)
-- Quality variants are already deduplicated in extraction (stored in slot.variants)
function HDG_PowerCrafting.GetCraftedRecipe(itemID)
    local entry = GetRecipeByItemID(itemID)
    if not entry then return nil end

    -- New v2 slot-based format
    if entry.slots then
        local reagents = {}
        for _, slot in ipairs(entry.slots) do
            -- Only include "basic" slots (required materials)
            -- Skip "modifying", "finishing", "automatic" (all optional)
            if slot.type == "basic" and slot.itemID then
                reagents[slot.itemID] = slot.qty or 1
            end
        end
        if next(reagents) then
            return reagents
        end
    end

    return nil
end

-- Return all decor recipes either the active character or any alt knows.
-- viewMode: "char" (only active char) | "account" (union of active + alts).
-- Each row: { itemID, name, spellID, profession, expansion, reagents,
--             knownByActive, bestAlt }
-- reagents is an array: { { id, qty } }
function HDG_PowerCrafting.GetAllKnownDecorRecipes(viewMode)
    BuildItemIDIndex()
    if not itemIDIndex then return {} end

    local out = {}
    local activeRealm = GetRealmName and GetRealmName() or ""
    local activeName  = UnitName and UnitName("player") or ""
    local activeKey   = activeName .. "-" .. activeRealm

    local function recipeKnown(charKey, spellID)
        if not spellID then return false end
        local chars = HDG_DB and HDG_DB.characters
        if not chars or not chars[charKey] then return false end
        local profs = chars[charKey].professions or {}
        for _, profData in pairs(profs) do
            if profData.knownRecipes and profData.knownRecipes[spellID] then
                return true
            end
        end
        return false
    end

    -- Normalize reagents from ProfessionsDB entry into an array { {id, qty} }.
    local function normalizeReagents(entry)
        local reagents = {}
        if entry.slots then
            for _, slot in ipairs(entry.slots) do
                if slot.type == "basic" and slot.itemID then
                    reagents[#reagents + 1] = { id = slot.itemID, qty = slot.qty }
                end
            end
        elseif entry.reagents then
            for rID, rData in pairs(entry.reagents) do
                reagents[#reagents + 1] = { id = rID, qty = rData.qty }
            end
        end
        return reagents
    end

    for itemID, recipeKey in pairs(itemIDIndex) do
        local entry = HDG_ProfessionsDB and HDG_ProfessionsDB[recipeKey]
        if entry then
            local spellID = entry.recipeID
            local knownByActive = recipeKnown(activeKey, spellID)
            local known = knownByActive
            local bestAlt = nil
            if not known and viewMode == "account" and HDG_DB and HDG_DB.characters then
                for charKey, _ in pairs(HDG_DB.characters) do
                    if charKey ~= activeKey and recipeKnown(charKey, spellID) then
                        known = true
                        bestAlt = bestAlt or charKey:match("^[^%-]+")
                        break
                    end
                end
            end
            if known then
                out[#out + 1] = {
                    itemID       = itemID,
                    name         = entry.name,
                    spellID      = spellID,
                    profession   = entry.profession,
                    expansion    = entry.expansion,
                    reagents     = normalizeReagents(entry),
                    knownByActive = knownByActive,
                    bestAlt      = bestAlt,
                }
            end
        end
    end
    return out
end

-- Normalize a recipe-shaped source into a Store-canonical queue item. Accepts
-- any record carrying { itemID, name, spellID, reagents, expansion, profession }
-- (HDG normalized recipes from GetAllKnownDecorRecipes / HDG_Data professions,
-- itemData from Preview tab, recipe rows from Acquire/Styles/VendorList — all
-- agree on these field names after the 2026-05-09 rename pass). The expansion
-- is run through HDG_ExpansionData.NormalizeToDisplayName so callers don't
-- need to remember whether their source emits "BFA" vs "Battle for Azeroth".
function HDG_PowerCrafting.MakeQueueItem(source, quantity)
    if not source or not source.itemID then return nil end
    local expansion = source.expansion
    if HDG_ExpansionData and HDG_ExpansionData.NormalizeToDisplayName then
        expansion = HDG_ExpansionData.NormalizeToDisplayName(expansion) or "Unknown"
    elseif not expansion then
        expansion = "Unknown"
    end
    return {
        itemID     = source.itemID,
        name       = source.name,
        spellID    = source.spellID,
        reagents   = source.reagents,
        expansion  = expansion,
        profession = source.profession,
        quantity   = quantity or 1,
    }
end

-- Build dependency graph for a decor recipe
-- Returns: { nodes = {}, edges = {}, leafNodes = {}, craftOrder = {} }
function HDG_PowerCrafting.BuildDependencyGraph(decorRecipe)
    if not decorRecipe or not decorRecipe.reagents then
        return nil, "Invalid recipe"
    end

    -- Check memoization cache (graphs are deterministic for same reagents)
    local cacheKey = GetReagentSignature(decorRecipe)
    if cacheKey and dependencyGraphCache[cacheKey] then
        return dependencyGraphCache[cacheKey]
    end

    local graph = {
        nodes = {},        -- itemID -> { id, name, qty, source, depth }
        edges = {},        -- { from = parentID, to = childID }
        leafNodes = {},    -- Items that don't need further crafting
        craftOrder = {},   -- Topological order (leaf first)
        rawMaterials = {}, -- Final raw materials needed
    }

    local visited = {}
    local inStack = {} -- For cycle detection

    -- Recursive function to build graph
    local function addNode(itemID, qty, depth, parentID)
        if depth > MAX_RECURSION_DEPTH then
            return false, "Max recursion depth exceeded"
        end

        -- Cycle detection
        if inStack[itemID] then
            -- Circular dependency detected - treat as leaf
            if not graph.nodes[itemID] then
                local name = HDG_PowerCrafting.GetItemName(itemID)
                graph.nodes[itemID] = {
                    id = itemID,
                    name = name,
                    qty = qty,
                    source = "Circular",
                    depth = depth,
                    isCircular = true,
                }
                graph.leafNodes[itemID] = true
            else
                graph.nodes[itemID].qty = graph.nodes[itemID].qty + qty
            end
            return true
        end

        inStack[itemID] = true

        -- Check if already processed
        if visited[itemID] then
            -- Add to existing quantity
            graph.nodes[itemID].qty = graph.nodes[itemID].qty + qty

            if graph.rawMaterials[itemID] then
                -- Leaf revisit: just bump the raw-material total.
                graph.rawMaterials[itemID].qty = graph.rawMaterials[itemID].qty + qty
            elseif graph.nodes[itemID].isCrafted then
                -- Crafted revisit: propagate the additional qty down through
                -- the existing children so deeper rawMaterials totals stay
                -- correct. Without this, a reagent reached via two paths
                -- (e.g. Bolt of Imbued Frostweave from both Spellweave and
                -- Moonshroud) only contributes its first path's leaves.
                local craftRecipe = HDG_PowerCrafting.GetCraftedRecipe(itemID)
                if craftRecipe then
                    for subItemID, subQty in pairs(craftRecipe) do
                        local ok, err = addNode(subItemID, subQty * qty, depth + 1, itemID)
                        if not ok then
                            inStack[itemID] = nil
                            return false, err
                        end
                    end
                end
            end

            -- Add edge if there's a parent
            if parentID then
                table.insert(graph.edges, { from = parentID, to = itemID, qty = qty })
            end

            inStack[itemID] = nil
            return true
        end

        visited[itemID] = true

        -- Get item info
        local name = HDG_PowerCrafting.GetItemName(itemID)
        local source = (HDG_ReagentsDB and HDG_ReagentsDB[itemID] and HDG_ReagentsDB[itemID][1]) or "Unknown"

        -- Create node
        graph.nodes[itemID] = {
            id = itemID,
            name = name,
            qty = qty,
            source = source,
            depth = depth,
        }

        -- Add edge if there's a parent
        if parentID then
            table.insert(graph.edges, { from = parentID, to = itemID, qty = qty })
        end

        -- Check if this is a crafted item with known recipe
        local craftRecipe = HDG_PowerCrafting.GetCraftedRecipe(itemID)
        local isCrafted = HDG_PowerCrafting.IsCraftedReagent(itemID)

        if isCrafted and craftRecipe and next(craftRecipe) then
            -- This is a crafted item - recurse into its components
            graph.nodes[itemID].isCrafted = true

            for subItemID, subQty in pairs(craftRecipe) do
                local success, err = addNode(subItemID, subQty * qty, depth + 1, itemID)
                if not success then
                    inStack[itemID] = nil
                    return false, err
                end
            end
        else
            -- This is a leaf node (gathering, vendor, or unknown crafted)
            graph.leafNodes[itemID] = true

            -- Add to raw materials
            if graph.rawMaterials[itemID] then
                graph.rawMaterials[itemID].qty = graph.rawMaterials[itemID].qty + qty
            else
                graph.rawMaterials[itemID] = {
                    id = itemID,
                    name = name,
                    qty = qty,
                    source = source,
                }
            end
        end

        inStack[itemID] = nil
        return true
    end

    -- Process all reagents in the decor recipe
    for reagentID, reagentData in pairs(decorRecipe.reagents) do
        local qty = reagentData.qty or 1
        local success, err = addNode(reagentID, qty, 0, nil)
        if not success then
            return nil, err
        end
    end

    -- Generate topological sort order (leaf nodes first)
    graph.craftOrder = HDG_PowerCrafting.TopologicalSort(graph)

    -- Cache the result for subsequent calls with same reagents
    if cacheKey then
        dependencyGraphCache[cacheKey] = graph
    end

    return graph
end

function HDG_PowerCrafting.InvalidateGraphCache()
    dependencyGraphCache = {}
end

-- ============================================================================
-- TOPOLOGICAL SORT (Kahn's Algorithm)
-- ============================================================================

function HDG_PowerCrafting.TopologicalSort(graph)
    if not graph or not graph.nodes then return {} end

    local result = {}
    local inDegree = {}
    local queue = {}

    -- Calculate in-degree for each node
    for itemID, _ in pairs(graph.nodes) do
        inDegree[itemID] = 0
    end

    local reverseAdj = {} -- toNode -> list of fromNodes
    for _, edge in ipairs(graph.edges) do
        if inDegree[edge.to] then
            inDegree[edge.to] = inDegree[edge.to] + 1
        end
        if not reverseAdj[edge.to] then reverseAdj[edge.to] = {} end
        reverseAdj[edge.to][#reverseAdj[edge.to] + 1] = edge.from
    end

    -- Find all nodes with in-degree 0 (leaf nodes)
    for itemID, degree in pairs(inDegree) do
        if degree == 0 then
            table.insert(queue, itemID)
        end
    end

    -- Process queue
    while #queue > 0 do
        local current = table.remove(queue, 1)
        table.insert(result, current)

        local dependents = reverseAdj[current]
        if dependents then
            for _, from in ipairs(dependents) do
                if inDegree[from] then
                    inDegree[from] = inDegree[from] - 1
                    if inDegree[from] == 0 then
                        table.insert(queue, from)
                    end
                end
            end
        end
    end

    return result
end

-- ============================================================================
-- RAW MATERIALS CALCULATION
-- ============================================================================

-- Get total raw materials needed for a crafting queue
function HDG_PowerCrafting.CalculateRawMaterials(craftingQueue)
    if not craftingQueue then return {} end

    local totalRawMaterials = {}

    for recipeKey, quantity in pairs(craftingQueue) do
        local recipe = HDG_DecorDB and HDG_DecorDB[tonumber(recipeKey)]
        if recipe and quantity > 0 then
            local graph = HDG_PowerCrafting.BuildDependencyGraph(recipe)
            if graph and graph.rawMaterials then
                for itemID, matData in pairs(graph.rawMaterials) do
                    if totalRawMaterials[itemID] then
                        totalRawMaterials[itemID].qty = totalRawMaterials[itemID].qty + (matData.qty * quantity)
                    else
                        totalRawMaterials[itemID] = {
                            id = itemID,
                            name = matData.name,
                            qty = matData.qty * quantity,
                            source = matData.source,
                        }
                    end
                end
            end
        end
    end

    return totalRawMaterials
end

-- ============================================================================
-- CRAFTING ORDER GENERATION
-- ============================================================================

-- Get ordered crafting steps for a recipe
-- Returns array of { itemID, name, qty, profession, step }
function HDG_PowerCrafting.GetCraftingOrder(decorRecipe)
    local graph = HDG_PowerCrafting.BuildDependencyGraph(decorRecipe)
    if not graph then return {} end

    local steps = {}
    local stepNum = 1

    -- Process in topological order
    for _, itemID in ipairs(graph.craftOrder) do
        local node = graph.nodes[itemID]
        if node and node.isCrafted then
            table.insert(steps, {
                step = stepNum,
                itemID = itemID,
                name = node.name,
                qty = node.qty,
                source = node.source,
            })
            stepNum = stepNum + 1
        end
    end

    return steps
end

-- ============================================================================
-- UTILITY FUNCTIONS
-- ============================================================================

-- Get item name - delegates to HDG_Data for caching, with local DB fallbacks
function HDG_PowerCrafting.GetItemName(itemID)
    -- Try HDG_Data cache first (shared with other modules)
    if HDG_Data and HDG_Data.GetCachedItemName then
        local name = HDG_Data.GetCachedItemName(itemID)
        if name and name ~= "Loading..." then
            return name
        end
    end

    -- O(1) lookup: ReagentsDB stores name as [2]
    if HDG_ReagentsDB and HDG_ReagentsDB[itemID] and HDG_ReagentsDB[itemID][2] then
        return HDG_ReagentsDB[itemID][2]
    end

    -- Check DecorDB reagents as fallback
    if HDG_DecorDB then
        for _, recipe in pairs(HDG_DecorDB) do
            if recipe.reagents and recipe.reagents[itemID] then
                return recipe.reagents[itemID].name
            end
        end
    end

    -- Check ProfessionsDB: slot name first (has actual item name), then recipe output
    if HDG_ProfessionsDB then
        local recipeMatch
        for _, recipe in pairs(HDG_ProfessionsDB) do
            if recipe.slots then
                for _, slot in ipairs(recipe.slots) do
                    if slot.itemID == itemID and slot.name then return slot.name end
                end
            end
            if recipe.itemID == itemID then recipeMatch = recipe.name end
        end
        if recipeMatch then return recipeMatch end
    end

    -- Return placeholder - HDG_Data will async load and cache it
    return "Item #" .. tostring(itemID)
end

-- Check if player can craft a specific intermediate item.
-- itemIDIndex stores the HDG_ProfessionsDB key (which equals itemID), so we
-- have to dereference the entry and pass entry.recipeID — the actual
-- Blizzard recipe spell ID — to PlayerKnowsRecipeBySpellID. Passing the
-- itemID directly used to silently always return false because spell IDs
-- and item IDs occupy different numeric ranges.
function HDG_PowerCrafting.CanPlayerCraft(itemID)
    BuildItemIDIndex()
    local key = itemIDIndex and itemIDIndex[itemID]
    if not key then return false end
    local entry = HDG_ProfessionsDB and HDG_ProfessionsDB[key]
    local recipeSpellID = entry and entry.recipeID
    if not recipeSpellID then return false end
    return HDG_Data.PlayerKnowsRecipeBySpellID(recipeSpellID) == true
end

-- Get detailed material breakdown with hierarchy
function HDG_PowerCrafting.GetMaterialBreakdown(decorRecipe)
    local graph = HDG_PowerCrafting.BuildDependencyGraph(decorRecipe)
    if not graph then return nil end

    local breakdown = {
        directMaterials = {},       -- First-level reagents
        intermediateMaterials = {}, -- Crafted reagents
        rawMaterials = {},          -- Final leaf materials
        craftingSteps = {},         -- Ordered crafting steps
    }

    -- Direct materials (from recipe)
    if decorRecipe.reagents then
        for itemID, data in pairs(decorRecipe.reagents) do
            local source = (HDG_ReagentsDB and HDG_ReagentsDB[itemID] and HDG_ReagentsDB[itemID][1]) or "Unknown"
            breakdown.directMaterials[itemID] = {
                id = itemID,
                name = data.name,
                qty = data.qty,
                source = source,
                isCrafted = (source == CRAFTED_SOURCE),
            }
        end
    end

    -- Intermediate materials (crafted reagents that need sub-crafting)
    for itemID, node in pairs(graph.nodes) do
        if node.isCrafted and node.depth > 0 then
            breakdown.intermediateMaterials[itemID] = {
                id = itemID,
                name = node.name,
                qty = node.qty,
                depth = node.depth,
            }
        end
    end

    -- Raw materials
    breakdown.rawMaterials = graph.rawMaterials

    -- Crafting steps
    breakdown.craftingSteps = HDG_PowerCrafting.GetCraftingOrder(decorRecipe)

    return breakdown
end

-- ============================================================================
-- MATERIAL AGGREGATION (extracted from HDG_RecipesTab Phase 1 refactor)
-- ============================================================================

-- Lumber item IDs (used by FilterAndSortMaterials to exclude lumber)
local LUMBER_IDS = HDG_ExpansionData.LUMBER_IDS

-- Subsidiary recipe helpers (used by crafting queue)
function HDG_PowerCrafting.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 crafting queue 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] and HDG_ReagentsDB[reagentID][1]

            if source == "Crafted" then
                local recipe = HDG_PowerCrafting.GetRecipeByItemID(reagentID)
                if recipe then
                    local qtyNeeded = (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] and HDG_ReagentsDB[reagentID][1]

            if source == "Crafted" then
                local recipe = HDG_PowerCrafting.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)
function HDG_PowerCrafting.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] and HDG_ReagentsDB[reagent.id][1]
            if source == "Crafted" and HDG_PowerCrafting.GetRecipeByItemID(reagent.id) then
                return true
            end
        end
    else
        for reagentID, _ in pairs(reagentsList) do
            local source = HDG_ReagentsDB and HDG_ReagentsDB[reagentID] and HDG_ReagentsDB[reagentID][1]
            if source == "Crafted" and HDG_PowerCrafting.GetRecipeByItemID(reagentID) then
                return true
            end
        end
    end
    return false
end

-- Aggregate materials from crafting queue
function HDG_PowerCrafting.AggregateMaterials(craftingQueue)
    local materials = {}

    for _, item in ipairs(craftingQueue) 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] and HDG_ReagentsDB[matID][1]
                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] and HDG_ReagentsDB[matID][1]) or "Unknown"
                        end

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

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

                    local recipeName = item.name or ("Item " .. (item.itemID 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

function HDG_PowerCrafting.AggregateMaterialsByRecipe(craftingQueue)
    local recipes = {}

    for _, item in ipairs(craftingQueue) do
        if item.reagents then
            local recipeName = item.name or ("Item " .. (item.itemID or "?"))
            local recipeEntry = {
                recipeName = recipeName,
                recipeID = item.itemID,
                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] and HDG_ReagentsDB[matID][1]

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

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

                    recipeEntry.materials[matID] = {
                        id = matID,
                        needed = amountForThis,
                        have = HDG_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_PowerCrafting.AggregateMaterialsByRecipeRaw(craftingQueue)
    local recipes = {}

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

            -- Convert crafting queue reagents format for Power Crafting
            local reagentsForGraph = {}
            for reagentKey, reagent in pairs(item.reagents) do
                local reagentID = reagent.id or reagentKey
                reagentsForGraph[reagentID] = {
                    name = reagent.name or ("Item " .. reagentID),
                    qty  = reagent.qty or 1,
                }
            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] and HDG_ReagentsDB[matID][1]) or "Unknown"
                    if matSource ~= "Ignore" then
                        local qty = rawMat.qty * item.quantity
                        recipeEntry.materials[matID] = {
                            id = matID,
                            needed = qty,
                            have = HDG_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] and HDG_ReagentsDB[matID][1]

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

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

                        recipeEntry.materials[matID] = {
                            id = matID,
                            needed = amountForThis,
                            have = HDG_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_PowerCrafting.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 = HDG_PowerCrafting.GetItemName(a.id) or ""
            local nameB = HDG_PowerCrafting.GetItemName(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_PowerCrafting.AggregateRawMaterials(craftingQueue)
    local materials = {}
    local intermediates = {} -- Crafted sub-components with their raw material children

    for _, item in ipairs(craftingQueue) do
        if item.reagents and HDG_PowerCrafting then
            -- Convert crafting queue 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="...", qty=5}
                -- Keyed format: reagentKey = 123, reagent = {name="...", qty=5}
                local reagentID = reagent.id or reagentKey
                reagentsForGraph[reagentID] = {
                    name = reagent.name or ("Item " .. reagentID),
                    qty  = reagent.qty or 1,
                }
            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 = HDG_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.itemID 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] and HDG_ReagentsDB[subMatID][1]) 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 = HDG_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.itemID 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
            end
        end
    end

    return materials, intermediates
end

function HDG_PowerCrafting.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 = HDG_PowerCrafting.GetItemName(a.id) or ""
        local bName = HDG_PowerCrafting.GetItemName(b.id) or ""
        return aName < bName
    end)

    return matArray
end

-- Categorize materials by source
function HDG_PowerCrafting.CategorizeMaterials(matArray)
    local categories = {
        vendor = { name = HDG_L["MAT_FROM_VENDOR"] or "From Vendor", icon = "Interface\\Icons\\INV_Misc_Coin_02", items = {} },
        gather = { name = HDG_L["MAT_FROM_GATHERING"] or "From Gathering / AH", icon = "Interface\\Icons\\INV_Misc_Map_01", items = {} },
        craft = { name = HDG_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 = HDG_PowerCrafting.GetItemName(a.id) or ""
            local bName = HDG_PowerCrafting.GetItemName(b.id) or ""
            return aName < bName
        end)
    end

    return categories
end

-- ============================================================================
-- BLIZZARD EVENT WIRING
-- Called once from HousingDecorGuide.lua's ADDON_LOADED handler.
-- ============================================================================

-- Tag any item tooltip whose itemID is a known housing-decor reagent with
-- a small "Housing Decor reagent - N recipes" line. Gated by the
-- tooltipDecorTag config flag.
function HDG_PowerCrafting.RegisterTooltipDecorator()
    if not TooltipDataProcessor or not TooltipDataProcessor.AddTooltipPostCall then return end
    TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item, function(tooltip, data)
        if tooltip ~= GameTooltip then return end
        local HDG = _G.HousingDecorGuide
        local store = HDG and HDG.Store
        local state = store and store:GetState()
        if not state or not state.config or not state.config.tooltipDecorTag then return end
        local itemID = data and data.id
        if not itemID then return end
        local recipes = HDG_PowerCrafting.GetRecipesForReagent(itemID)
        if not recipes or #recipes == 0 then return end
        tooltip:AddLine(string.format("|cFF14b8a6Housing Decor reagent - %d recipe%s|r",
            #recipes, #recipes == 1 and "" or "s"))
    end)
end

-- Crafting queue auto-decrement on craft completion. When a decor item is
-- crafted, find the matching queue row and reduce its quantity (or remove
-- it if zero). Also handles the "close profession window after craft"
-- cleanup when the craft was initiated from inside the addon's UI
-- (privateDB.pendingCraftInfo tracks that).
--
-- Talking-head craft quotes are NOT handled here -- they're a Vamoose/UI
-- concern and stay in the main file's event router for now.
function HDG_PowerCrafting.RegisterCraftAutoDecrement()
    local f = CreateFrame("Frame")
    f:RegisterEvent("TRADE_SKILL_ITEM_CRAFTED_RESULT")
    f:SetScript("OnEvent", function(_, _, result)
        if not result or not result.hyperlink then return end
        local itemID = tonumber(result.hyperlink:match("item:(%d+)"))
        if not itemID then return end

        local decorInfo = privateDB.decorItem and privateDB.decorItem[itemID]
        if not decorInfo then return end

        local HDG = _G.HousingDecorGuide
        local store = HDG and HDG.Store
        local state = store and store:GetState()
        local queue = state and state.craftingQueue
        if not queue then return end

        local craftedQty = result.quantity or 1
        for _, item in ipairs(queue) do
            if item.itemID == itemID then
                local newQty = item.quantity - craftedQty
                if newQty <= 0 then
                    store:RemoveFromCraftingQueue(itemID)
                    print(string.format("|cFF14b8a6[HDG]|r %s removed from crafting queue (crafting complete)",
                        item.name or "Item"))
                else
                    store:UpdateCraftingQueueItemQty(itemID, newQty)
                    print(string.format("|cFF14b8a6[HDG]|r %s: %d remaining in crafting queue",
                        item.name or "Item", newQty))
                end

                -- Addon-initiated crafts track remaining count so we can close
                -- the profession window only after the LAST craft of a batch.
                local pending = privateDB.pendingCraftInfo
                if pending and pending.itemID == itemID then
                    pending.remaining = pending.remaining - craftedQty
                    if pending.remaining <= 0 then
                        pending.itemID = nil
                        pending.remaining = 0
                        local closeAfterCraft = state.config and state.config.closeProfWindowAfterCraft
                        if closeAfterCraft == nil then closeAfterCraft = true end
                        if closeAfterCraft then
                            C_Timer.After(0.5, function()
                                if InCombatLockdown() then return end
                                if ProfessionsFrame and ProfessionsFrame:IsShown() then
                                    HideUIPanel(ProfessionsFrame)
                                elseif C_TradeSkillUI and C_TradeSkillUI.CloseTradeSkill then
                                    C_TradeSkillUI.CloseTradeSkill()
                                end
                            end)
                        end
                    end
                end

                break
            end
        end

        -- Refresh lumber display after queue change.
        if HDG_UI and HDG_UI.RefreshLumberTracking then HDG_UI.RefreshLumberTracking() end
    end)
end
