local PT = AhUI
if not PT then return end

PT.ProfessionGuides = PT.ProfessionGuides or {}
local G = PT.ProfessionGuides

local PG_DEBUG_RECIPE_IDS = {}

local function PG_DebugEnabled(recipe)
    return type(recipe) == "table" and recipe.spellID and PG_DEBUG_RECIPE_IDS[recipe.spellID] or false
end

local function PG_Debug(recipe, fmt, ...)
    if not PG_DebugEnabled(recipe) then return end
    local msg = fmt
    if select('#', ...) > 0 then
        local ok, formatted = pcall(string.format, fmt, ...)
        msg = ok and formatted or tostring(fmt)
    end
    print("AhUI PG DEBUG [" .. tostring(recipe.spellID) .. "] " .. tostring(msg))
end


local function PG_DebugDumpTable(recipe, label, value, depth, visited)
    if not PG_DebugEnabled(recipe) then return end
    depth = depth or 0
    visited = visited or {}
    if depth > 2 then
        PG_Debug(recipe, "%s=<max depth>", tostring(label))
        return
    end
    if type(value) ~= "table" then
        PG_Debug(recipe, "%s=%s", tostring(label), tostring(value))
        return
    end
    if visited[value] then
        PG_Debug(recipe, "%s=<cycle>", tostring(label))
        return
    end
    visited[value] = true
    local parts = {}
    for k, v in pairs(value) do
        local piece
        if type(v) == "table" then
            piece = tostring(k) .. "=<table>"
        else
            piece = tostring(k) .. "=" .. tostring(v)
        end
        parts[#parts + 1] = piece
    end
    table.sort(parts)
    PG_Debug(recipe, "%s={%s}", tostring(label), table.concat(parts, ", "))
    for k, v in pairs(value) do
        if type(v) == "table" then
            PG_DebugDumpTable(recipe, tostring(label) .. "." .. tostring(k), v, depth + 1, visited)
        end
    end
end


PT.ProfessionGuides_RacialBonuses = {
    [6]  = { [182] = 5 },        -- Tauren - Herbalism
    [7]  = { [202] = 5 },        -- Gnome - Engineering
    [9]  = { [171] = 5 },        -- Goblin - Alchemy
    [10] = { [333] = 5 },        -- Blood Elf - Enchanting
    [11] = { [755] = 5 },        -- Draenei - Jewelcrafting
    [22] = { [393] = 5 },        -- Worgen - Skinning
    [24] = { [185] = 5 },        -- Pandaren (A) - Cooking
    [25] = { [185] = 5 },        -- Pandaren (H) - Cooking
    [27] = { [773] = 5 },        -- Nightborne - Inscription
    [28] = { [186] = 5 },        -- Highmountain Tauren - Mining
    [30] = { [164] = 5 },        -- Lightforged Draenei - Blacksmithing
    [32] = { ALL = 2 },          -- Kul Tiran - ALL professions
    [34] = { [164] = 5 },        -- Dark Iron Dwarf - Blacksmithing
}

G.state = {
    selectedSkillLineID = nil,
    manualStepIndex = nil, 
	selectedProfession = nil,
}

function G:SetSkillLine(skillLineID)
    self.state.selectedSkillLineID = skillLineID
    self.state.manualStepIndex = nil
end

function G:SetProfession(prof)
    self.state.selectedProfession = prof
    self.state.selectedSkillLineID = nil 
    self.state.manualStepIndex = nil
end

function G:GetSelectedProfession()
    return self.state.selectedProfession
end

function G:GetSelectedSkillLine()
    return self.state.selectedSkillLineID
end

function G:GetRacialBonusForSkillLine(skillLineID)
    if not skillLineID then return 0 end

    local parentID = self:GetParentProfessionID(skillLineID)
    if not parentID then return 0 end

    local raceID = select(3, UnitRace("player"))
    if not raceID then return 0 end

	local raceBonuses = PT.ProfessionGuides_RacialBonuses[raceID]
	if not raceBonuses then return 0 end

	if raceBonuses.ALL then
		return raceBonuses.ALL
	end

	return raceBonuses[parentID] or 0

end

function G:GetEffectiveSkillLevel(skillLineID)
    if not skillLineID then
        return 0, 0, 0
    end

    if not C_TradeSkillUI
       or not C_TradeSkillUI.GetProfessionInfoBySkillLineID
    then
        return 0, 0, 0
    end

    local info = C_TradeSkillUI.GetProfessionInfoBySkillLineID(skillLineID)
    if not info then
        return 0, 0, 0
    end

    local realSkill = info.skillLevel or 0
    local bonus = self:GetRacialBonusForSkillLine(skillLineID) or 0

    return math.max(0, realSkill - bonus), realSkill, bonus
end


function G:GetSkillForStep(skillLineID, step)
    local effective, real = self:GetEffectiveSkillLevel(skillLineID)

    if step and step.useEffectiveSkill then
        return effective
    end

    return real
end

G.guides = {}

G.PROFESSION_ICONS = {
    [171] = 4620669, -- Alchemy
    [164] = 4620670, -- Blacksmithing
    [333] = 4620672, -- Enchanting
    [202] = 4620673, -- Engineering
    [773] = 4620676, -- Inscription
    [755] = 4620677, -- Jewelcrafting
    [165] = 4620678, -- Leatherworking
    [197] = 4620681, -- Tailoring
    [185] = 4620671, -- Cooking
}

function G:GetCurrentStepBySkillLine(skillLineID)
self:ValidateManualStep(skillLineID)

	if self.state.manualStepIndex then
		local steps, guide = self:GetStepsBySkillLine(skillLineID)
		local idx = self.state.manualStepIndex

		if steps and steps[idx] then
			return steps[idx], false, guide
		end
	end

		if not skillLineID then return end

	local effectiveSkill, realSkill =
		self:GetEffectiveSkillLevel(skillLineID)

	for _, expansion in pairs(self.guides) do
		for _, guide in pairs(expansion) do
			if guide.skillLineID == skillLineID then

				for _, step in ipairs(guide.steps) do
					local skillForStep = self:GetSkillForStep(skillLineID, step)

					if skillForStep >= step.min and skillForStep < step.max then
						return step, skillForStep, guide
					end
				end


			end
		end
	end
end	

function G:GetRequiredRecipeItemForStep(step)
    if not step or not step.recipes then return nil end

    for _, recipe in ipairs(step.recipes) do
        if recipe.requires then
            for _, req in ipairs(recipe.requires) do
                if req.type == "recipe_item" then
                    return req.itemID
                end
            end
        end
    end

    return nil
end

function G:GetRecipeVendor(skillLineID, recipeItemID)
    local guide = G:GetGuideBySkillLine(skillLineID)
    if not guide or not guide.vendors then return nil end

    local faction =
        UnitFactionGroup("player") == "Alliance"
        and "alliance"
        or "horde"

    local vendors = guide.vendors[faction]
    if not vendors then return nil end

    for _, vendor in ipairs(vendors) do
        if vendor.sells then
            for _, item in ipairs(vendor.sells) do
                if item.itemID == recipeItemID then
                    return vendor
                end
            end
        end
    end

    return nil
end

function G:GetRecipeVendorForStep(skillLineID, step)
    if not step or not step.recipes then return nil end

    local guide = self:GetGuideBySkillLine(skillLineID)
    if not guide or not guide.vendors then return nil end

    local faction =
        UnitFactionGroup("player") == "Alliance"
        and "alliance"
        or "horde"

    local vendors = guide.vendors[faction]
    if not vendors then return nil end

    for _, recipe in ipairs(step.recipes) do

        if recipe.recipeSource == "quest" then
            return nil
        end

        if recipe.requires then
            for _, req in ipairs(recipe.requires) do

                if req.type == "recipe_item" and req.itemID then
                    if recipe.spellID and self:IsRecipeLearned(recipe.spellID) then
                        return nil
                    end
                    return self:GetRecipeVendor(skillLineID, req.itemID)
                end

                if req.type == "recipe_spell" and req.spellID then
                    if self:IsRecipeLearned(req.spellID) then
                        return nil
                    end

                    for _, vendor in ipairs(vendors) do
                        if vendor.teaches then
                            for _, t in ipairs(vendor.teaches) do
                                if t.spellID == req.spellID then
                                    return vendor
                                end
                            end
                        end
                    end
                end

            end
        end
    end

    return nil
end

function G:GetRemainingCraftsForStep(skillLineID, step)
    if not step or not step.recipes or not step.recipes[1] then
        return 0
    end

    local recipe = step.recipes[1]
    local totalCrafts = recipe.crafts or 0
    if totalCrafts <= 0 then
        return 0
    end

    local skill = self:GetSkillForStep(skillLineID, step)
    if type(skill) ~= "number" then
        return totalCrafts
    end

    local done = math.max(0, skill - step.min)

    return math.max(0, totalCrafts - done)
end

function G:GetPlannedCraftsForRecipe(skillLineID, step, recipe)
    if not step or not recipe then
        return 0
    end

    if step.onlyStepShopping
       and step.recipes
       and #step.recipes > 1
    then
        return math.max(0, recipe.crafts or 0)
    end

    local remaining = self:GetRemainingCraftsForStep(skillLineID, step) or 0
    if remaining <= 0 then
        return 0
    end

    if recipe.crafts and recipe.crafts > 0 then
        return math.min(recipe.crafts, remaining)
    end

    return remaining
end 

function G:GetStepsBySkillLine(skillLineID)
    if not skillLineID then return end

    for _, expansion in pairs(self.guides) do
        for _, guide in pairs(expansion) do
            if guide.skillLineID == skillLineID then
                return guide.steps, guide
            end
        end
    end
end

function G:GetGuideBySkillLine(skillLineID)
    if not skillLineID then return end

    for _, expansion in pairs(self.guides) do
        for _, guide in pairs(expansion) do
            if guide.skillLineID == skillLineID then
                return guide
            end
        end
    end
end

function G:IsGuideCompleted(skillLineID)
    if not skillLineID then return false end

    local guide = self:GetGuideBySkillLine(skillLineID)
    if not guide or not guide.steps or #guide.steps == 0 then
        return false
    end

    local lastStep = guide.steps[#guide.steps]
    if not lastStep or not lastStep.max then
        return false
    end

    local profInfo =
        C_TradeSkillUI
        and C_TradeSkillUI.GetProfessionInfoBySkillLineID
        and C_TradeSkillUI.GetProfessionInfoBySkillLineID(skillLineID)

	local skill = self:GetSkillForStep(skillLineID, lastStep)

	if type(skill) ~= "number" then
		return false
	end

	return skill >= lastStep.max

end

function G:GetCurrentStepIndex(skillLineID)
    if self.state.manualStepIndex then
        return self.state.manualStepIndex
    end

    local steps = self:GetStepsBySkillLine(skillLineID)
    if not steps then return end

    for i, step in ipairs(steps) do
        local skill = self:GetSkillForStep(skillLineID, step)

        if skill >= step.min and skill < step.max then
            return i
        end
    end
end

function G:GetAutoStepIndex(skillLineID)
	if not skillLineID then return end

	local steps = self:GetStepsBySkillLine(skillLineID)
	if not steps then return end

	for i, step in ipairs(steps) do
		local skill = self:GetSkillForStep(skillLineID, step)

		if skill >= step.min and skill < step.max then
			return i
		end
	end
end

function G:ValidateManualStep(skillLineID)
    if not self.state.manualStepIndex then return end

    local steps = self:GetStepsBySkillLine(skillLineID)
    if not steps then
        self.state.manualStepIndex = nil
        return
    end

    if not steps[self.state.manualStepIndex] then
        self.state.manualStepIndex = nil
    end
end

function G:StepPrev()
    local skillLineID = self:GetSelectedSkillLine()
    if not skillLineID then return end

    local steps = self:GetStepsBySkillLine(skillLineID)
    if not steps then return end

    local idx = self.state.manualStepIndex
    or self:GetCurrentStepIndex(skillLineID)
    or 1

    if idx > 1 then
        self.state.manualStepIndex = idx - 1
    end
end

function G:StepNext()
    local skillLineID = self:GetSelectedSkillLine()
    if not skillLineID then return end

    local steps = self:GetStepsBySkillLine(skillLineID)
    if not steps then return end

    local idx = self.state.manualStepIndex
    or self:GetCurrentStepIndex(skillLineID)
    or 1

    if idx < #steps then
        self.state.manualStepIndex = idx + 1
    end
end

function G:ResetManualStep()
    self.state.manualStepIndex = nil
end

function G:GetTotalMaterials(skillLineID)
    local totals = {}

    local startIndex = self.state.manualStepIndex

    if not startIndex then
        local step = self:GetCurrentStepBySkillLine(skillLineID)
        if step then
            local steps = self:GetStepsBySkillLine(skillLineID)
            for i, s in ipairs(steps or {}) do
                if s == step then
                    startIndex = i
                    break
                end
            end
        end
    end

    startIndex = startIndex or 1

    for _, expansion in pairs(self.guides) do
        for _, guide in pairs(expansion) do
            if guide.skillLineID == skillLineID then

                for idx, step in ipairs(guide.steps) do
                    if idx >= startIndex
                       and not step.disablesShopping
                       and step.recipes
                    then
                        for _, recipe in ipairs(step.recipes) do
                            if recipe.reagents then

								local crafts = self:GetPlannedCraftsForRecipe(skillLineID, step, recipe)

                                if crafts > 0 then
                                    for _, r in ipairs(recipe.reagents) do
                                        if not r.crafted then

                                            if r.anyOf then
                                                local need = (r.count or 0) * crafts

                                                if r.isMilling then
                                                    for _, opt in ipairs(r.anyOf) do
                                                        totals[opt.itemID] =
                                                            (totals[opt.itemID] or 0) + need
                                                    end
                                                else
                                                    local have = 0
                                                    for _, opt in ipairs(r.anyOf) do
                                                        have = have +
                                                            (GetItemCount(opt.itemID, true) or 0)
                                                    end

                                                    local missing =
                                                        math.max(0, need - have)

                                                    if missing > 0 then
                                                        local first = r.anyOf[1]
                                                        if first and first.itemID then
                                                            totals[first.itemID] =
                                                                (totals[first.itemID] or 0) + missing
                                                        end
                                                    end
                                                end

                                            else
                                                totals[r.itemID] =
                                                    (totals[r.itemID] or 0)
                                                    + ((r.count or 0) * crafts)
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end

            end
        end
    end

    return totals
end

function G:GetMaterialsForCurrentStep(skillLineID)
    local totals = {}

    local step = self:GetCurrentStepBySkillLine(skillLineID)
    if not step or not step.recipes then
        return totals
    end

    for _, recipe in ipairs(step.recipes) do
        local crafts = self:GetPlannedCraftsForRecipe(skillLineID, step, recipe)

        local recipeReagents = self:GetRecipeReagentsForUse(skillLineID, recipe)
        if crafts > 0 and recipeReagents then
            for _, r in ipairs(recipeReagents) do
                if not r.crafted then
                    if r.anyOf then
                        local need = (r.count or 0) * crafts

                        if r.isMilling then
                            for _, opt in ipairs(r.anyOf) do
                                totals[opt.itemID] =
                                    (totals[opt.itemID] or 0) + need
                            end
                        else
                            local first = r.anyOf[1]
                            if first then
                                totals[first.itemID] =
                                    (totals[first.itemID] or 0) + need
                            end
                        end
                    else
                        totals[r.itemID] =
                            (totals[r.itemID] or 0)
                            + ((r.count or 0) * crafts)
                    end
                end
            end
        end
    end

    return totals
end

function G:GetItemCountTotal(itemID)
    if not itemID then return 0 end
    return GetItemCount(itemID, true) or 0
end

G._resolvedRecipeCache = G._resolvedRecipeCache or {}
G._resolvedOutputCache = G._resolvedOutputCache or {}
G._recipeDataRequests = G._recipeDataRequests or {}

local function PG_GetRecipeCacheBucket(cache, skillLineID)
    if not skillLineID then
        return nil
    end

    cache[skillLineID] = cache[skillLineID] or {}
    return cache[skillLineID]
end

local function PG_GetRecipeCacheKey(recipe)
    if not recipe then
        return "0:0"
    end

    return tostring(recipe.spellID or 0) .. ":" .. tostring(recipe.recipeLevel or 0)
end

local function PG_ExtractItemIDFromLink(link)
    if not link then
        return nil
    end

    local itemID = select(1, GetItemInfoInstant(link))
    if type(itemID) == "number" and itemID > 0 then
        return itemID
    end

    return tonumber(string.match(link, "item:(%d+)"))
end

local function PG_ExtractItemIDFromReagentData(reagent)
    if type(reagent) ~= "table" then
        return nil
    end

    local direct = {
        reagent.itemID,
        reagent.reagentItemID,
        reagent.id,
        reagent.item and reagent.item.itemID,
        reagent.itemInfo and reagent.itemInfo.itemID,
        reagent.reagent and reagent.reagent.itemID,
        reagent.data and reagent.data.itemID,
    }

    for _, value in ipairs(direct) do
        if type(value) == "number" and value > 0 then
            return value
        end
    end

    local links = {
        reagent.itemLink,
        reagent.hyperlink,
        reagent.link,
        reagent.item and reagent.item.itemLink,
        reagent.itemInfo and reagent.itemInfo.hyperlink,
        reagent.reagent and reagent.reagent.itemLink,
    }

    for _, link in ipairs(links) do
        local itemID = PG_ExtractItemIDFromLink(link)
        if itemID then
            return itemID
        end
    end

    return nil
end

local function PG_ExtractItemIDsFromReagentData(reagent, out, seen, depth)
    out = out or {}
    seen = seen or {}
    depth = depth or 0

    if depth > 3 or reagent == nil then
        return out
    end

    if type(reagent) == "number" then
        if reagent > 0 and not seen[reagent] then
            seen[reagent] = true
            table.insert(out, reagent)
        end
        return out
    end

    if type(reagent) == "string" then
        local itemID = PG_ExtractItemIDFromLink(reagent)
        if itemID and not seen[itemID] then
            seen[itemID] = true
            table.insert(out, itemID)
        end
        return out
    end

    if type(reagent) ~= "table" then
        return out
    end

    local single = PG_ExtractItemIDFromReagentData(reagent)
    if single and not seen[single] then
        seen[single] = true
        table.insert(out, single)
    end

    local collections = {
        reagent.itemIDs,
        reagent.items,
        reagent.reagents,
        reagent.reagentItems,
        reagent.data and reagent.data.items,
        reagent.data and reagent.data.reagents,
        reagent.itemInfo and reagent.itemInfo.itemIDs,
    }

    for _, collection in ipairs(collections) do
        if type(collection) == "table" then
            for key, value in pairs(collection) do
                if type(key) == "number" or key == "itemID" or key == "itemIDs" then
                    PG_ExtractItemIDsFromReagentData(value, out, seen, depth + 1)
                end
            end
        end
    end

    return out
end


local function PG_CopyOptionList(anyOf)
    local out = {}
    for _, opt in ipairs(anyOf or {}) do
        if type(opt) == "table" and opt.itemID then
            local copy = { itemID = opt.itemID }
            if opt.tier ~= nil then
                copy.tier = opt.tier
            end
            table.insert(out, copy)
        end
    end
    return out
end

local function PG_CopyReagentList(reagents)
    local out = {}
    for _, reagent in ipairs(reagents or {}) do
        if type(reagent) == "table" then
            local copy = {
                count = reagent.count,
                crafted = reagent.crafted,
                isMilling = reagent.isMilling,
            }

            if reagent.itemID then
                copy.itemID = reagent.itemID
            end
            if reagent.tier ~= nil then
                copy.tier = reagent.tier
            end
            if reagent.anyOf then
                copy.anyOf = PG_CopyOptionList(reagent.anyOf)
            end

            table.insert(out, copy)
        end
    end
    return out
end

function G:ClearResolvedRecipeCache(skillLineID)
    if skillLineID then
        self._resolvedRecipeCache[skillLineID] = nil
        self._resolvedOutputCache[skillLineID] = nil
        self._recipeDataRequests[skillLineID] = nil
    else
        self._resolvedRecipeCache = {}
        self._resolvedOutputCache = {}
        self._recipeDataRequests = {}
    end
end

function G:RequestRecipeData(skillLineID)
    if not skillLineID or not C_TradeSkillUI or not C_TradeSkillUI.OpenTradeSkill then
        return false
    end

    local now = GetTime and GetTime() or 0
    local last = self._recipeDataRequests[skillLineID]
    if last and (now - last) < 0.35 then
        return false
    end

    self._recipeDataRequests[skillLineID] = now
    local ok = pcall(C_TradeSkillUI.OpenTradeSkill, skillLineID)
    return ok and true or false
end

function G:IsRecipeDataReady(skillLineID)
    if not C_TradeSkillUI or not C_TradeSkillUI.IsTradeSkillReady then
        return false
    end

    if not C_TradeSkillUI.IsTradeSkillReady() then
        return false
    end

    if C_TradeSkillUI.IsDataSourceChanging and C_TradeSkillUI.IsDataSourceChanging() then
        return false
    end

    return true
end

local function PG_GetRecipeOutputItemIDs(self, skillLineID, recipe)
    if not recipe or not recipe.spellID then
        return nil
    end

    local bucket = PG_GetRecipeCacheBucket(self._resolvedOutputCache, skillLineID)
    local key = PG_GetRecipeCacheKey(recipe)
    if bucket and bucket[key] then
        return bucket[key]
    end

    if not self:IsRecipeDataReady(skillLineID) then
        self:RequestRecipeData(skillLineID)
        return nil
    end

    local out = {}

    if C_TradeSkillUI and C_TradeSkillUI.GetRecipeQualityItemIDs then
        local ok, qualityItemIDs = pcall(C_TradeSkillUI.GetRecipeQualityItemIDs, recipe.spellID)
        if ok and type(qualityItemIDs) == "table" then
            for _, itemID in ipairs(qualityItemIDs) do
                if type(itemID) == "number" and itemID > 0 then
                    out[itemID] = true
                end
            end
        end
    end

    if not next(out) and C_TradeSkillUI and C_TradeSkillUI.GetRecipeItemLink then
        local ok, link = pcall(C_TradeSkillUI.GetRecipeItemLink, recipe.spellID)
        if ok and link then
            local itemID = PG_ExtractItemIDFromLink(link)
            if itemID then
                out[itemID] = true
            end
        end
    end

    if bucket then
        bucket[key] = out
    end

    return out
end

function G:GetGuideCraftedItemIDs(skillLineID)
    if not skillLineID then
        return nil
    end

    local bucket = PG_GetRecipeCacheBucket(self._resolvedOutputCache, skillLineID)
    if bucket and bucket.__guideCrafted then
        return bucket.__guideCrafted
    end

    local guide = self:GetGuideBySkillLine(skillLineID)
    if not guide or not guide.steps then
        return nil
    end

    local crafted = {}
    local hadAny = false

    for _, step in ipairs(guide.steps) do
        for _, recipe in ipairs(step.recipes or {}) do
            if recipe.outputItemIDs then
                for _, itemID in ipairs(recipe.outputItemIDs) do
                    if type(itemID) == "number" and itemID > 0 then
                        crafted[itemID] = true
                        hadAny = true
                    end
                end
            end

            local outputIDs = PG_GetRecipeOutputItemIDs(self, skillLineID, recipe)
            if outputIDs then
                for itemID in pairs(outputIDs) do
                    crafted[itemID] = true
                    hadAny = true
                end
            end
        end
    end

    if bucket then
        bucket.__guideCrafted = hadAny and crafted or {}
        return bucket.__guideCrafted
    end

    return hadAny and crafted or {}
end

local function PG_ShouldInferCrafted(self, skillLineID, reagent)
    if type(reagent) ~= "table" then
        return false
    end

    if reagent.crafted ~= nil then
        return reagent.crafted and true or false
    end

    local craftedIDs = self:GetGuideCraftedItemIDs(skillLineID)
    if not craftedIDs then
        return false
    end

    if reagent.anyOf then
        for _, opt in ipairs(reagent.anyOf) do
            if opt and craftedIDs[opt.itemID] then
                return true
            end
        end
        return false
    end

    return reagent.itemID and craftedIDs[reagent.itemID] and true or false
end


local function PG_GetSpellDescriptionText(spellID)
    if not spellID then
        return nil
    end

    local desc = nil

    if C_Spell and C_Spell.GetSpellDescription then
        local okDesc, value = pcall(C_Spell.GetSpellDescription, spellID)
        if okDesc and type(value) == "string" and value ~= "" then
            desc = value
        end
    end

    if (not desc or desc == "") and GetSpellDescription then
        local okDesc, value = pcall(GetSpellDescription, spellID)
        if okDesc and type(value) == "string" and value ~= "" then
            desc = value
        end
    end

    return desc
end

local function PG_GetProcessingCountFromSpellDescription(spellID)
    local desc = PG_GetSpellDescriptionText(spellID)
    if type(desc) ~= "string" or desc == "" then
        return nil
    end

    local numberText = desc:match("(%d+)")
    local value = tonumber(numberText)
    if value and value > 0 then
        return value
    end

    return nil
end

local function PG_BuildResolvedReagentsFromSalvageAPI(self, skillLineID, recipe)
    if not recipe or not recipe.spellID then
        return nil
    end
    if not C_TradeSkillUI or not C_TradeSkillUI.GetSalvagableItemIDs then
        PG_Debug(recipe, "GetSalvagableItemIDs unavailable")
        return nil
    end

    PG_Debug(recipe, "salvage resolver start skillLineID=%s", tostring(skillLineID))

    if C_TradeSkillUI.OpenRecipe then
        local okOpen, openErr = pcall(C_TradeSkillUI.OpenRecipe, recipe.spellID)
        PG_Debug(recipe, "OpenRecipe ok=%s err=%s", tostring(okOpen), tostring(openErr))
    end

    local okIDs, itemIDs = pcall(C_TradeSkillUI.GetSalvagableItemIDs, recipe.spellID)
    PG_Debug(recipe, "GetSalvagableItemIDs ok=%s type=%s count=%s", tostring(okIDs), type(itemIDs), tostring(type(itemIDs)=="table" and #itemIDs or nil))
    if type(itemIDs) == "table" then
        local ids = {}
        for i, itemID in ipairs(itemIDs) do ids[#ids+1] = tostring(itemID) end
        PG_Debug(recipe, "salvage itemIDs=%s", table.concat(ids, ","))
    end
    if not okIDs or type(itemIDs) ~= "table" or #itemIDs == 0 then
        return nil
    end

    local count = nil
    if C_TradeSkillUI.GetRecipeNumReagents and C_TradeSkillUI.GetRecipeReagentInfo then
        local okNum, numReagents = pcall(C_TradeSkillUI.GetRecipeNumReagents, recipe.spellID)
        PG_Debug(recipe, "GetRecipeNumReagents ok=%s num=%s", tostring(okNum), tostring(numReagents))
        if okNum and type(numReagents) == "number" and numReagents > 0 then
            for idx = 1, numReagents do
                local okInfo, reagentName, reagentTexture, reagentCount, playerReagentCount, reagentIsOptional, reagentQuality = pcall(C_TradeSkillUI.GetRecipeReagentInfo, recipe.spellID, idx)
                PG_Debug(recipe, "ReagentInfo[%d] ok=%s name=%s count=%s player=%s optional=%s quality=%s tex=%s", idx, tostring(okInfo), tostring(reagentName), tostring(reagentCount), tostring(playerReagentCount), tostring(reagentIsOptional), tostring(reagentQuality), tostring(reagentTexture))
                if idx == 1 and okInfo and type(reagentCount) == "number" and reagentCount > 0 then
                    count = reagentCount
                end
            end
        end
    else
        PG_Debug(recipe, "GetRecipeNumReagents or GetRecipeReagentInfo unavailable")
    end

    if C_TradeSkillUI.GetRecipeRequirements then
        local okReq, requirements = pcall(C_TradeSkillUI.GetRecipeRequirements, recipe.spellID)
        PG_Debug(recipe, "GetRecipeRequirements ok=%s type=%s", tostring(okReq), type(requirements))
        if okReq then
            PG_DebugDumpTable(recipe, "requirements", requirements)
        end
    else
        PG_Debug(recipe, "GetRecipeRequirements unavailable")
    end

    if C_TradeSkillUI.GetRecipeReagentSlotInfo then
        for idx = 1, 5 do
            local okSlot, slotInfo = pcall(C_TradeSkillUI.GetRecipeReagentSlotInfo, recipe.spellID, idx)
            PG_Debug(recipe, "GetRecipeReagentSlotInfo[%d] ok=%s type=%s", idx, tostring(okSlot), type(slotInfo))
            if okSlot and slotInfo then
                PG_DebugDumpTable(recipe, "slotInfo[" .. tostring(idx) .. "]", slotInfo)
                if type(slotInfo) == "table" and type(slotInfo.quantityRequired) == "number" and slotInfo.quantityRequired > 0 and not count then
                    count = slotInfo.quantityRequired
                end
            end
        end
    else
        PG_Debug(recipe, "GetRecipeReagentSlotInfo unavailable")
    end

    if C_TradeSkillUI.GetRecipeFixedReagentItemLink then
        for idx = 1, 5 do
            local okLink, itemLink = pcall(C_TradeSkillUI.GetRecipeFixedReagentItemLink, recipe.spellID, idx)
            PG_Debug(recipe, "GetRecipeFixedReagentItemLink[%d] ok=%s link=%s", idx, tostring(okLink), tostring(itemLink))
        end
    else
        PG_Debug(recipe, "GetRecipeFixedReagentItemLink unavailable")
    end


    local function InferSalvageCount(ids, apiCount)
        if type(apiCount) == "number" and apiCount > 0 then
            return apiCount
        end

        local spellDescCount = PG_GetProcessingCountFromSpellDescription(recipe.spellID)
        if type(spellDescCount) == "number" and spellDescCount > 0 then
            return spellDescCount
        end

        if type(ids) ~= "table" or #ids == 0 or not GetItemInfoInstant then
            return tonumber(apiCount) or 1
        end

        local tradeGoodsClass = _G.LE_ITEM_CLASS_TRADEGOODS
        local gemClass = _G.LE_ITEM_CLASS_GEM
        local herbSubclass = _G.LE_ITEM_TRADEGOODS_HERB
        local metalStoneSubclass = _G.LE_ITEM_TRADEGOODS_METAL_AND_STONE

        local allHerbs = true
        local allOreLike = true
        local allGems = true

        for _, itemID in ipairs(ids) do
            local _, _, _, _, _, _, _, _, _, _, _, classID, subClassID = GetItemInfoInstant(itemID)

            if classID ~= tradeGoodsClass or subClassID ~= herbSubclass then
                allHerbs = false
            end
            if classID ~= tradeGoodsClass or subClassID ~= metalStoneSubclass then
                allOreLike = false
            end
            if classID ~= gemClass then
                allGems = false
            end
        end

        if allHerbs or allOreLike then
            return 5
        end
        if allGems then
            return 1
        end

        return tonumber(apiCount) or 1
    end

    local inferredCount = InferSalvageCount(itemIDs, count)
    PG_Debug(recipe, "count before infer=%s after infer=%s", tostring(count), tostring(inferredCount))
    count = inferredCount

    local manual = recipe.reagents and recipe.reagents[1]
    local options = {}
    for _, itemID in ipairs(itemIDs) do
        if type(itemID) == "number" and itemID > 0 then
            table.insert(options, { itemID = itemID })
        end
    end

    if #options == 0 then
        PG_Debug(recipe, "no valid options after salvage IDs filtering")
        return nil
    end

    local reagent
    if #options == 1 then
        reagent = {
            itemID = options[1].itemID,
            count = count,
        }
    else
        reagent = {
            anyOf = options,
            count = count,
        }
    end

    if type(manual) == "table" then
        if manual.crafted ~= nil then reagent.crafted = manual.crafted end
        if manual.isMilling then reagent.isMilling = true end
    end

    if reagent.crafted == nil then
        reagent.crafted = PG_ShouldInferCrafted(self, skillLineID, reagent)
    end

    if reagent.anyOf then
        PG_Debug(recipe, "resolved anyOf count=%s options=%s", tostring(reagent.count), tostring(#reagent.anyOf))
    else
        PG_Debug(recipe, "resolved itemID=%s count=%s", tostring(reagent.itemID), tostring(reagent.count))
    end

    return { reagent }
end

local function PG_BuildResolvedReagentsFromLegacyAPI(self, skillLineID, recipe)
    if not recipe or not recipe.spellID then
        return nil
    end

    local salvageResolved = PG_BuildResolvedReagentsFromSalvageAPI(self, skillLineID, recipe)
    if salvageResolved then
        return salvageResolved
    end

    if not C_TradeSkillUI or not C_TradeSkillUI.GetRecipeNumReagents or not C_TradeSkillUI.GetRecipeReagentItemLink then
        return nil
    end

    local okNum, numReagents = pcall(C_TradeSkillUI.GetRecipeNumReagents, recipe.spellID)
    if not okNum or type(numReagents) ~= "number" or numReagents <= 0 then
        return nil
    end

    local resolved = {}
    for index = 1, numReagents do
        local count = nil
        if C_TradeSkillUI.GetRecipeReagentInfo then
            local okInfo, _, _, reagentCount = pcall(C_TradeSkillUI.GetRecipeReagentInfo, recipe.spellID, index)
            if okInfo and type(reagentCount) == "number" then
                count = reagentCount
            end
        end

        local okLink, link = pcall(C_TradeSkillUI.GetRecipeReagentItemLink, recipe.spellID, index)
        local itemID = okLink and PG_ExtractItemIDFromLink(link) or nil
        if itemID and (count or 0) > 0 then
            local reagent = { itemID = itemID, count = count }
            local manual = recipe.reagents and recipe.reagents[index]
            if type(manual) == "table" then
                if manual.crafted ~= nil then reagent.crafted = manual.crafted end
                if manual.isMilling then reagent.isMilling = true end
            end
            if reagent.crafted == nil then
                reagent.crafted = PG_ShouldInferCrafted(self, skillLineID, reagent)
            end
            table.insert(resolved, reagent)
        end
    end

    if #resolved == 0 then
        return nil
    end

    return resolved
end

local function PG_ShouldIncludeSchematicSlot(slot, recipe)
    if type(recipe) == "table" then
        if recipe.includeAllReagentTypes then
            return true
        end
        if recipe.includeFinishingReagents and slot and Enum and Enum.CraftingReagentType and slot.reagentType == Enum.CraftingReagentType.Finishing then
            return true
        end
        if recipe.includeModifyingReagents and slot and Enum and Enum.CraftingReagentType and slot.reagentType == Enum.CraftingReagentType.Modifying then
            return true
        end
        if recipe.includeAutomaticReagents and slot and Enum and Enum.CraftingReagentType and slot.reagentType == Enum.CraftingReagentType.Automatic then
            return true
        end
    end

    if not slot or not Enum or not Enum.CraftingReagentType then
        return true
    end

    return slot.reagentType == nil or slot.reagentType == Enum.CraftingReagentType.Basic
end

local function PG_BuildResolvedReagentsFromSchematic(self, skillLineID, recipe)
    if not recipe or not recipe.spellID then
        return nil
    end
    if not C_TradeSkillUI or not C_TradeSkillUI.GetRecipeSchematic then
        return nil
    end

    local ok, schematic = pcall(C_TradeSkillUI.GetRecipeSchematic, recipe.spellID, false, recipe.recipeLevel)
    if (not ok or type(schematic) ~= "table" or type(schematic.reagentSlotSchematics) ~= "table")
       and C_TradeSkillUI.OpenRecipe
    then
        pcall(C_TradeSkillUI.OpenRecipe, recipe.spellID)
        ok, schematic = pcall(C_TradeSkillUI.GetRecipeSchematic, recipe.spellID, false, recipe.recipeLevel)
    end

    if not ok or type(schematic) ~= "table" or type(schematic.reagentSlotSchematics) ~= "table" then
        return nil
    end

    local resolved = {}
    local resolvedIndex = 0

    for _, slot in ipairs(schematic.reagentSlotSchematics) do
        if PG_ShouldIncludeSchematicSlot(slot, recipe) then
            local options = {}
            local quantity = nil

            for _, reagentData in ipairs(slot.reagents or {}) do
                local itemIDs = PG_ExtractItemIDsFromReagentData(reagentData)
                if #itemIDs > 0 then
                    local slotQuantity = nil
                    if type(slot.GetQuantityRequired) == "function" then
                        local okQty, value = pcall(slot.GetQuantityRequired, slot, reagentData)
                        if okQty and type(value) == "number" then
                            slotQuantity = value
                        end
                    end

                    quantity = quantity or slotQuantity or slot.quantityRequired or slot.requiredQuantity or reagentData.quantityRequired or reagentData.quantity

                    local tier = reagentData.tier or reagentData.quality or reagentData.reagentQuality
                    for _, itemID in ipairs(itemIDs) do
                        local option = { itemID = itemID }
                        if tier ~= nil then
                            option.tier = tier
                        end
                        table.insert(options, option)
                    end
                end
            end

            if #options == 0 then
                local fallbackItemIDs = PG_ExtractItemIDsFromReagentData(slot)
                for _, itemID in ipairs(fallbackItemIDs) do
                    table.insert(options, { itemID = itemID })
                end
            end

            quantity = tonumber(quantity) or 0
            if quantity > 0 and #options > 0 then
                resolvedIndex = resolvedIndex + 1

                local manual = recipe.reagents and recipe.reagents[resolvedIndex]
                local reagent
                if #options == 1 then
                    reagent = {
                        itemID = options[1].itemID,
                        count = quantity,
                    }
                    if options[1].tier ~= nil then
                        reagent.tier = options[1].tier
                    end
                else
                    reagent = {
                        anyOf = options,
                        count = quantity,
                    }
                end

                if type(manual) == "table" then
                    if manual.crafted ~= nil then reagent.crafted = manual.crafted end
                    if manual.isMilling then reagent.isMilling = true end
                end

                if reagent.crafted == nil then
                    reagent.crafted = PG_ShouldInferCrafted(self, skillLineID, reagent)
                end

                table.insert(resolved, reagent)
            end
        end
    end

    if #resolved == 0 then
        return nil
    end

    return resolved
end

function G:GetResolvedRecipeReagents(skillLineID, recipe)
    if not recipe then
        return nil, "missing"
    end

    if recipe.autoReagents == false or not recipe.spellID then
        if recipe.reagents then
            return recipe.reagents, "manual"
        end
        return nil, "missing"
    end

    local bucket = PG_GetRecipeCacheBucket(self._resolvedRecipeCache, skillLineID)
    local key = PG_GetRecipeCacheKey(recipe)
    if bucket and bucket[key] then
        return bucket[key], "resolved"
    end

    if not self:IsRecipeDataReady(skillLineID) then
        self:RequestRecipeData(skillLineID)
        return nil, "loading"
    end

    local resolved = PG_BuildResolvedReagentsFromSchematic(self, skillLineID, recipe)
    if not resolved and C_TradeSkillUI and C_TradeSkillUI.OpenRecipe then
        pcall(C_TradeSkillUI.OpenRecipe, recipe.spellID)
        resolved = PG_BuildResolvedReagentsFromSchematic(self, skillLineID, recipe)
    end
    if not resolved then
        resolved = PG_BuildResolvedReagentsFromLegacyAPI(self, skillLineID, recipe)
    end

    if resolved then
        local copied = PG_CopyReagentList(resolved)
        if bucket then
            bucket[key] = copied
        end
        return copied, "resolved"
    end

    if recipe.reagents then
        return recipe.reagents, "manual"
    end

    return nil, "missing"
end

function G:GetRecipeReagentsForUse(skillLineID, recipe)
    local reagents = self:GetResolvedRecipeReagents(skillLineID, recipe)
    return reagents or recipe.reagents or nil
end

local function PG_GetAuctionPriceByItemID(itemID)
    if not itemID then return nil end
    if not Auctionator or not Auctionator.API or not Auctionator.API.v1 then
        return nil
    end
    if not Auctionator.API.v1.GetAuctionPriceByItemID then
        return nil
    end

    local ok, price = pcall(Auctionator.API.v1.GetAuctionPriceByItemID, "AhUI ProfessionGuide", itemID)
    if ok and type(price) == "number" and price > 0 then
        return price
    end

    return nil
end

function G:GetAuctionPriceByItemID(itemID)
    return PG_GetAuctionPriceByItemID(itemID)
end

local function PG_GetAnyOfTier(skillLineID, reagent, opt, index)
    if opt and opt.tier ~= nil then
        return opt.tier
    end

    if not skillLineID or not reagent or not reagent.anyOf then
        return nil
    end

    local count = #reagent.anyOf
    if count < 2 or count > 5 then
        return nil
    end

    local Epochs = PT.ProfessionGuides_Epochs
    local expansionName = nil
    if Epochs then
        for _, professionData in pairs(Epochs) do
            if type(professionData) == "table" and professionData.data then
                local data = professionData.data[skillLineID]
                if data then
                    if data.nameKey == "PG_EPOCH_DRAGONFLIGHT" then
                        expansionName = "Dragonflight"
                        break
                    elseif data.nameKey == "PG_EPOCH_TWW" then
                        expansionName = "The War Within"
                        break
                    elseif data.name then
                        expansionName = data.name
                        break
                    end
                end
            end
        end
    end

    if expansionName == "Dragonflight" or expansionName == "The War Within" then
        return index
    end

    return nil
end

local function PG_GetCheapestAnyOfOption(skillLineID, reagent)
    if not reagent or not reagent.anyOf or #reagent.anyOf == 0 then
        return nil, nil
    end

    local bestOpt, bestTier, bestPrice = nil, nil, nil

    for index, opt in ipairs(reagent.anyOf) do
        if opt and opt.itemID then
            local price = PG_GetAuctionPriceByItemID(opt.itemID)
            local tier = PG_GetAnyOfTier(skillLineID, reagent, opt, index)

            if price and (not bestPrice or price < bestPrice) then
                bestPrice = price
                bestOpt = opt
                bestTier = tier
            end
        end
    end

    if bestOpt then
        return bestOpt, bestTier
    end

    local first = reagent.anyOf[1]
    return first, PG_GetAnyOfTier(skillLineID, reagent, first, 1)
end

local function PG_AddShoppingEntry(out, itemID, quantity, tier)
    if not itemID or not quantity or quantity <= 0 then
        return
    end

    local entry = out[itemID]
    if not entry then
        entry = {
            itemID = itemID,
            quantity = 0,
        }
        out[itemID] = entry
    end

    entry.quantity = entry.quantity + quantity

    if tier ~= nil then
        entry.tier = tier
    end
end

local function PG_GetInventoryCount(cache, itemID)
    local value = cache[itemID]
    if value == nil then
        value = GetItemCount(itemID, true) or 0
        cache[itemID] = value
    end
    return value
end

local function PG_ConsumeInventory(cache, itemID, quantity)
    local have = PG_GetInventoryCount(cache, itemID)
    local used = math.min(have, quantity)
    cache[itemID] = have - used
    return used
end


local function PG_CopyAnyOfOptions(anyOf)
    local out = {}
    if not anyOf then
        return out
    end

    for idx, opt in ipairs(anyOf) do
        if type(opt) == "table" and opt.itemID then
            table.insert(out, {
                itemID = opt.itemID,
                tier = opt.tier ~= nil and opt.tier or idx,
            })
        end
    end

    table.sort(out, function(a, b)
        if (a.itemID or 0) == (b.itemID or 0) then
            return (a.tier or 0) < (b.tier or 0)
        end
        return (a.itemID or 0) < (b.itemID or 0)
    end)

    return out
end

local function PG_MakeAnyOfGroupKey(options)
    local parts = {}
    for _, opt in ipairs(options or {}) do
        table.insert(parts, tostring(opt.itemID or 0) .. ":" .. tostring(opt.tier or 0))
    end
    return table.concat(parts, "|")
end

local function PG_GetRowSortName(entry)
    if not entry then return "" end

    if entry.label and entry.label ~= "" then
        return entry.label
    end

    if entry.itemID then
        return GetItemInfo(entry.itemID) or ("item:" .. tostring(entry.itemID))
    end

    if entry.options and entry.options[1] and entry.options[1].itemID then
        return GetItemInfo(entry.options[1].itemID) or ("item:" .. tostring(entry.options[1].itemID))
    end

    return ""
end

function G:BuildShoppingList(skillLineID)
    local rows = {}

    if not skillLineID then
        return rows
    end

    local guide = self:GetGuideBySkillLine(skillLineID)
    if not guide or not guide.steps then
        return rows
    end

    local grouped = {}
    local inventory = {}
    local hadLoading = false
    local currentStep = self:GetCurrentStepBySkillLine(skillLineID)

    local startIndex = self.state.manualStepIndex
    if not startIndex and currentStep then
        for i, step in ipairs(guide.steps) do
            if step == currentStep then
                startIndex = i
                break
            end
        end
    end
    startIndex = startIndex or 1

    local function AddItemRow(itemID, count)
        if not itemID or not count or count <= 0 then
            return
        end

        local key = "item:" .. tostring(itemID)
        local entry = grouped[key]
        if not entry then
            entry = {
                itemID = itemID,
                quantity = 0,
            }
            grouped[key] = entry
        end

        entry.quantity = (entry.quantity or 0) + count
    end

    local function AddAnyOfRow(anyOf, count)
        if not anyOf or not count or count <= 0 then
            return
        end

        local options = PG_CopyAnyOfOptions(anyOf)
        if #options == 0 then
            return
        end

        local key = "group:" .. PG_MakeAnyOfGroupKey(options)
        local entry = grouped[key]
        if not entry then
            entry = {
                options = options,
                quantity = 0,
                label = PG_GetRowSortName({ options = options }),
            }
            grouped[key] = entry
        end

        entry.quantity = (entry.quantity or 0) + count
    end

    local function CollectStep(step)
        if not step or step.disablesShopping or not step.recipes then
            return
        end

        for _, recipe in ipairs(step.recipes) do
            local crafts = self:GetPlannedCraftsForRecipe(skillLineID, step, recipe)
            local recipeReagents, reagentState = self:GetResolvedRecipeReagents(skillLineID, recipe)

            if reagentState == "loading" and not recipeReagents then
                hadLoading = true
            end

            if crafts > 0 and recipeReagents then
                for _, r in ipairs(recipeReagents) do
                    if not r.crafted then
                        local need = (r.count or 0) * crafts
                        if need > 0 then
                            if r.anyOf then
                                local remainingNeed = need

                                for _, opt in ipairs(r.anyOf) do
                                    if remainingNeed <= 0 then
                                        break
                                    end
                                    if opt and opt.itemID then
                                        remainingNeed = remainingNeed - PG_ConsumeInventory(inventory, opt.itemID, remainingNeed)
                                    end
                                end

                                if remainingNeed > 0 then
                                    AddAnyOfRow(r.anyOf, remainingNeed)
                                end
                            elseif r.itemID then
                                local used = PG_ConsumeInventory(inventory, r.itemID, need)
                                local missing = need - used
                                if missing > 0 then
                                    AddItemRow(r.itemID, missing)
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    if currentStep and currentStep.onlyStepShopping then
        CollectStep(currentStep)
    else
        for idx = startIndex, #guide.steps do
            CollectStep(guide.steps[idx])
        end
    end

    for _, entry in pairs(grouped) do
        if entry.itemID then
            table.insert(rows, {
                itemID = entry.itemID,
                quantity = entry.quantity,
                label = PG_GetRowSortName(entry),
            })
        elseif entry.options then
            table.insert(rows, {
                options = entry.options,
                quantity = entry.quantity,
                label = entry.label,
            })
        end
    end

    table.sort(rows, function(a, b)
        local an = PG_GetRowSortName(a)
        local bn = PG_GetRowSortName(b)
        if an == bn then
            local aid = a.itemID or (a.options and a.options[1] and a.options[1].itemID) or 0
            local bid = b.itemID or (b.options and b.options[1] and b.options[1].itemID) or 0
            return aid < bid
        end
        return an < bn
    end)

    if #rows == 0 and hadLoading then
        return nil, "loading"
    end

    if hadLoading then
        return rows, "partial_loading"
    end

    return rows
end

function G:ExportState()
    return {
        skillLineID     = self.state.selectedSkillLineID,
        manualStepIndex = self.state.manualStepIndex,
		profession = self.state.selectedProfession,

    }
end

function G:ImportState(data)
    if not data then return end

    self.state.selectedSkillLineID = data.skillLineID
    self.state.manualStepIndex     = data.manualStepIndex
	self.state.selectedProfession = data.profession

end

function G:GetPlayerFaction()
    local f = UnitFactionGroup("player")
    return f and f:lower()
end

function G:IsProfessionOpenForSkillLine(skillLineID)
    if not skillLineID then return false end

    if not ProfessionsFrame or not ProfessionsFrame:IsShown() then
        return false
    end

    if not C_TradeSkillUI
       or not C_TradeSkillUI.GetProfessionInfoBySkillLineID
    then
        return false
    end

    local info = C_TradeSkillUI.GetProfessionInfoBySkillLineID(skillLineID)
    return info ~= nil
end

function G:GetParentProfessionID(skillLineID)
    if not skillLineID then return end

    if not C_TradeSkillUI
       or not C_TradeSkillUI.GetProfessionInfoBySkillLineID
    then
        return
    end

    local info = C_TradeSkillUI.GetProfessionInfoBySkillLineID(skillLineID)
    return info and info.parentProfessionID
end

function G:IsProfessionWindowOpen(skillLineID)
    if not skillLineID then return false end
    return self:IsProfessionOpenForSkillLine(skillLineID)
end

function G:GetProfessionIcon(skillLineID)
    if not skillLineID then return end

    local parentID = self:GetParentProfessionID(skillLineID)
    if not parentID then return end

    return self.PROFESSION_ICONS[parentID]
end

function G:IsRecipeLearned(spellID)
    if not spellID then return false end

    if not C_TradeSkillUI
       or not C_TradeSkillUI.GetRecipeInfo
    then
        return false
    end

    local info = C_TradeSkillUI.GetRecipeInfo(spellID)
    if not info then
        return false
    end

    return info.learned == true
end

function G:GetMaxCraftsByMaterials(recipe, skillLineID)
    if not recipe then
        return 0
    end
    if recipe.reagentsAny then
        return 0
    end

    skillLineID = skillLineID or self:GetSelectedSkillLine()

    local recipeReagents = nil
    if skillLineID then
        recipeReagents = self:GetResolvedRecipeReagents(skillLineID, recipe)
    end

    local reagents = recipeReagents or recipe.reagents
    if not reagents or #reagents == 0 then
        return 0
    end

    local maxCrafts = math.huge

	for _, r in ipairs(reagents) do
		if not r.crafted then
			local perCraft = math.max(1, tonumber(r.count) or 0)

			if r.anyOf then
				local totalHave = 0

				for _, opt in ipairs(r.anyOf) do
					local itemID = opt and opt.itemID
					if itemID then
						totalHave = totalHave + self:GetItemCountTotal(itemID)
					end
				end

				local can = math.floor(totalHave / perCraft)
				maxCrafts = math.min(maxCrafts, can)
			else
				local itemID = r.itemID
				local have = itemID and self:GetItemCountTotal(itemID) or 0
				local can = math.floor(have / perCraft)
				maxCrafts = math.min(maxCrafts, can)
			end
		end
	end

    if maxCrafts == math.huge then
        return 0
    end

    return math.max(0, maxCrafts)
end

function G:GetStepNoteRefs(step)
    if not step or not step.noteRefs then
        return nil
    end

    local refs = {}

    for _, ref in ipairs(step.noteRefs) do

        if ref.type == "item" and ref.itemID then
            table.insert(refs, {
                type   = "item",
                itemID = ref.itemID,
                action = ref.action,
                count  = ref.count,
            })

        elseif ref.type == "spell" and ref.spellID then
            table.insert(refs, {
                type    = "spell",
                spellID = ref.spellID,
                action  = ref.action,
            })

        elseif ref.type == "npc"
           and ref.mapID
           and ref.x
           and ref.y
        then
            table.insert(refs, {
                type  = "npc",
                npcID = ref.npcID,
                name  = ref.name,
                mapID = ref.mapID,
                x     = ref.x,
                y     = ref.y,
            })
        end
    end

    return refs
end
