local Helpers = require("BC/Helpers")
local Options = require("BC/Options")

local INGREDIENT_ICON = getTexture("media/textures/context_ingredients.png")
local SEASONING_ICON  = getTexture("media/textures/context_seasonings.png")

local Butchering = require("BC/Butchering")

local function _cookToken(item)
    if item and item.isCooked and item:isCooked() then
        return "cooked"
    end
    return "raw"
end

local _Q_RANK = { poor = 3, medium = 2, prime = 1 }
local function _qualityRankFromItem(item)
    local q = Butchering.getMeatQuality and Butchering.getMeatQuality(item) or nil
    return _Q_RANK[q] or 0
end

local function BuildRecord(evoItem, evorecipe2, playerObj, cookingLvl)
    if not evoItem then return nil end

    ------------------------------------------------
    -- First, classify seasoning vs non-seasoning
    ------------------------------------------------
    local isSeasoning = Helpers.isSeasoning(evoItem)

    local use = 0
    local h   = evoItem.getHungerChange and evoItem:getHungerChange() or 0

    if isSeasoning and h < 0 then
        -- Seasonings: use HungerChange as remaining "power"
        local absUnits = math.abs(h) * 100
        use = math.floor(absUnits + 0.5)   -- 0 decimal places, rounded

    elseif instanceof(evoItem, "Food") then
        use = ISInventoryPaneContextMenu.getRealEvolvedItemUse(evoItem, evorecipe2, cookingLvl) or 0

    elseif instanceof(evoItem, "DrainableComboItem") and evoItem.getCurrentUses then
        use = evoItem:getCurrentUses() or 0
    end

    local record = {}

    record.item         = evoItem
    record.fullType     = evoItem:getFullType()
    record.displayName  = evoItem:getName()
    record.use          = use
    record.timeToRot    = Helpers.getTimeLeft(evoItem)
    record.isFood       = instanceof(evoItem, "Food")
    record.scriptItem   = evoItem:getScriptItem()
    record.isSeasoning  = isSeasoning

    -- Poison / bleach flags (for UI + sandbox handling)
    if record.isFood then
        record.isPoison       = evoItem.isPoison and evoItem:isPoison() or false
        record.poisonLevel    = evoItem.getPoisonLevelForRecipe and evoItem:getPoisonLevelForRecipe() or nil
        record.herbalistType  = evoItem.getHerbalistType and evoItem:getHerbalistType() or nil
    else
        record.isPoison = evoItem.isPoison and evoItem:isPoison() or false
    end

    record.type = evoItem:getType()

    Helpers.log(
        string.format("type=%s | isSeasoning=%s | uses=%s | timeToRot=%s | displayName=%s", tostring(record.fullType), tostring(record.isSeasoning), tostring(record.use), tostring(record.timeToRot), tostring(record.displayName))
    )
    return record
end

---------------------------------------------------------------
-- Helper: apply sandbox poisoning rules and derive extraInfo
--
-- Returns:
--   evoItemOrNil, extraInfo string
---------------------------------------------------------------

local function FilterPoisonAndLabel(record, baseItem, playerObj)
    local item = record.item
    local extraInfo = ""

    local enablePoisoning = SandboxVars and SandboxVars.EnablePoisoning or 1
    local disablePoison = (enablePoisoning == 2)

    -- Bleach griefing toggle (vanilla: mode 3)
    if enablePoisoning == 3 and item and record.type == "Bleach" then
        disablePoison = true
    end

    -- Food poison labels
    if record.isFood then
        if record.isSeasoning and not record.isPoison then
            -- Spice-only food
        elseif record.poisonLevel and record.isPoison then
            if disablePoison then
                item = nil
            else
                if record.herbalistType and item.getHerbalistType and item:getHerbalistType()
                    and playerObj:isRecipeActuallyKnown("Herbalist") then
                    extraInfo = getText("ContextMenu_EvolvedRecipe_Poison")
                end
            end
        elseif not record.isPoison and not record.isSeasoning then
            -- Non-poison, non-spice food: show (use) count later per-group.
        end
    else
        -- Non-food poison
        if record.isPoison and disablePoison then
            item = nil
        end
    end

    -- Special-case chum: vanilla clears extraInfo
    if item and baseItem and baseItem:getFullType() == "Base.Chum" then
        extraInfo = ""
    end

    return item, extraInfo
end

---------------------------------------------------------------
-- Helper: create a group record in a single place of truth
---------------------------------------------------------------
local function MakeGroup(bucket, bestRec, timeToRot, use, count, extraInfo, kind)
    return {
        bucket     = bucket,
        bestRecord = bestRec,
        timeToRot  = timeToRot or 9999999,
        use        = use or 0,
        count      = count or 1,
        extraInfo  = extraInfo or "",
        label      = bucket.displayName,
        kind       = kind or "main",
    }
end

---------------------------------------------------------------
-- Helper: append "(use)" and "[xN]" suffixes consistently
--
--   extraInfo: base string (may be empty)
--   use:       number or nil
--   count:     integer or nil
--   showQty:   boolean
---------------------------------------------------------------
local function AppendUseAndQuantity(extraInfo, use, count, showQty)
    local txt = extraInfo or ""

    if use and use > 0 then
        if txt ~= "" then
            txt = txt .. " "
        end
        txt = txt .. "(" .. tostring(use) .. ")"
    end

    if showQty and count and count > 1 then
        txt = txt .. " [x" .. tostring(count) .. "]"
    end

    return txt
end

---------------------------------------------------------------
-- Helper: build buckets (seasoningsByType / ingredientsByType)
--
-- Output:
--   {
--     seasonings = { [fullType] = { key, displayName, records = {...} } },
--     ingredients = { [fullType] = { key, displayName, records = {...} } },
--   }
---------------------------------------------------------------

local function BuildBuckets(evorecipe2, baseItem, playerObj, cookingLvl, items)
    if not items or items:isEmpty() then
        return nil
    end

    local seasonings = {}
    local ingredients = {}

    for i = 0, items:size() - 1 do
        local evoItem = items:get(i)
        local rec = BuildRecord(evoItem, evorecipe2, playerObj, cookingLvl)
        if rec then
            local bucketTable = rec.isSeasoning and seasonings or ingredients

            -- Default bucket key: full type
            local key = rec.fullType

            if not rec.isSeasoning then
                local it = rec.item

                -- Always split FOOD ingredients by cooked state (fixes cooked/uncooked bundling)
                if rec.isFood and it then
                    key = key .. "||" .. _cookToken(it)
                end

                -- Optional: split butcher-meat variants by stable meatQuality token (no localization fragility)
                if Options.shouldSeparateIngredientQualities() and it then
                    local q = Butchering.getMeatQuality and Butchering.getMeatQuality(it) or nil
                    if q then
                        key = key .. "||" .. tostring(q)
                    end
                end
            end

            local bucket = bucketTable[key]

            if not bucket then
                bucket = {
                    key         = key,
                    displayName = rec.displayName,
                    records     = {},
                }
                bucketTable[key] = bucket
            end
            table.insert(bucket.records, rec)
        end
    end

    return {
        seasonings  = seasonings,
        ingredients = ingredients,
    }
end

---------------------------------------------------------------
-- BuildSeasoningGroups
--
-- Input:
--   buckets   - table of { displayName, records = {record,...} }
--   baseItem  - the base dish item
--   playerObj - IsoPlayer (for poison / sandbox rules)
--
-- Output:
--   groups = {
--       {
--           bucket     = bucket,
--           bestRecord = rec,
--           timeToRot  = number,
--           use        = number,
--           count      = integer used for [xN],
--           extraInfo  = string,
--           label      = bucket.displayName,
--       },
--       ...
--   }
--
-- Behaviour:
--   - Always separates GOOD vs ROTTEN internally.
--   - Builds at most ONE good group and ONE rotten group per bucket.
--   - Good group: spoilage/uses logic as before.
--   - Rotten group: highest use first, earliest rot tiebreak.
--   - Quantity [xN] is never mixed between good/rotten.
---------------------------------------------------------------
local function BuildSeasoningGroups(buckets, baseItem, playerObj)
    local showQty        = Options.shouldShowQuantity()
    local sortBySpoilage = true --OPS.shouldSortBySpoilage()
    local sortByUses     = true --OPS.shouldSortByUsesRemaining()
    local showRotten     = Options.shouldShowRottenSeasoningGroup()

    local groups = {}
    local itemForTexture = nil

    for _, bucket in pairs(buckets) do
        local goodRecords   = {}
        local rottenRecords = {}

        ------------------------------------------------
        -- Classify records into good vs rotten,
        -- applying poison / sandbox filtering.
        ------------------------------------------------
        for _, rec in ipairs(bucket.records) do
            local item, extraInfo = FilterPoisonAndLabel(rec, baseItem, playerObj)
            if item then
                rec.extraInfoBase = extraInfo

                local evoItem = rec.item
                local isRotten = evoItem.isRotten and evoItem:isRotten() or false

                if isRotten then
                    table.insert(rottenRecords, rec)
                else
                    table.insert(goodRecords, rec)
                end
            end
        end

        ------------------------------------------------
        -- GOOD seasoning group
        ------------------------------------------------
        if #goodRecords > 0 then
            local bestRec  = nil
            local bestTime = nil
            local bestUse  = nil
            local count    = #goodRecords

            for _, rec in ipairs(goodRecords) do
                local t = rec.timeToRot
                local u = rec.use or 0

                if not bestRec then
                    bestRec  = rec
                    bestTime = t
                    bestUse  = u
                else
                    local better = false

                    if sortBySpoilage and sortByUses then
                        -- 1) Spoilage primary
                        if t < bestTime then
                            better = true
                        -- 2) Uses secondary when spoilage ties (least uses first)
                        elseif t == bestTime and u < bestUse then
                            better = true
                        end

                    elseif sortBySpoilage then
                        if t < bestTime then
                            better = true
                        end

                    elseif sortByUses then
                        if u < bestUse then
                            better = true
                        end
                    end

                    if better then
                        bestRec  = rec
                        bestTime = t
                        bestUse  = u
                    end
                end
            end

            if bestRec then
                local extraInfo = bestRec.extraInfoBase or ""

                -- "Spice" label
                if bestRec.isSeasoning then
                    local spiceText = getText("ContextMenu_EvolvedRecipe_Spice")
                    if extraInfo == "" then
                        extraInfo = spiceText
                    else
                        extraInfo = extraInfo .. " " .. spiceText
                    end
                end

                -- Append "(use)" and "[xN]" via helper (no behaviour change)
                extraInfo = AppendUseAndQuantity(extraInfo, bestUse, count, showQty)

                table.insert(groups, MakeGroup(
                    bucket,
                    bestRec,
                    bestTime,
                    bestUse,
                    count,
                    extraInfo,
                    "main"      -- good seasoning
                ))

                -- Prefer a GOOD seasoning as icon candidate
                if not itemForTexture and bestRec.item then
                    itemForTexture = bestRec.item
                end
            end
        end

        ------------------------------------------------
        -- ROTTEN seasoning group (single consolidated)
        ------------------------------------------------
        if showRotten and #rottenRecords > 0 then
            local bestRec  = nil
            local bestTime = nil
            local bestUse  = nil
            local count    = #rottenRecords

            for _, rec in ipairs(rottenRecords) do
                local t = rec.timeToRot
                local u = rec.use or 0

                if not bestRec then
                    bestRec  = rec
                    bestTime = t
                    bestUse  = u
                else
                    local better = false

                    -- Rotten: primary = HIGHEST use, tiebreaker = earliest rot
                    if u > bestUse then
                        better = true
                    elseif u == bestUse and sortBySpoilage and t < bestTime then
                        better = true
                    end

                    if better then
                        bestRec  = rec
                        bestTime = t
                        bestUse  = u
                    end
                end
            end

            if bestRec then
                local extraInfo = bestRec.extraInfoBase or ""

                if bestRec.isSeasoning then
                    local spiceText = getText("ContextMenu_EvolvedRecipe_Spice")
                    if extraInfo == "" then
                        extraInfo = spiceText
                    else
                        extraInfo = extraInfo .. " " .. spiceText
                    end
                end

                extraInfo = AppendUseAndQuantity(extraInfo, bestUse, count, showQty)

                table.insert(groups, MakeGroup(
                    bucket,
                    bestRec,
                    bestTime,
                    bestUse,
                    count,
                    extraInfo,
                    "rotten"    -- rotten seasoning
                ))

                -- Only use rotten as icon if no good candidate exists
                if not itemForTexture and bestRec.item then
                    itemForTexture = bestRec.item
                end
            end
        end
    end

    return groups, itemForTexture
end

---------------------------------------------------------------
-- BuildIngredientGroups
--
-- Splits:
--   - GOOD vs ROTTEN (using evoItem:isRotten())
--   - Within GOOD: FULL vs PARTIAL (by max use)
--
-- Options:
--   - OPS.shouldShowQuantity()
--   - OPS.shouldShowPartialIngredients()
--   - OPS.shouldShowRottenIngredientGroup()
--
-- Output groups follow the same shape as seasoning groups and
-- are consumed by AddIngredientsToMenu.
---------------------------------------------------------------
local function BuildIngredientGroups(buckets, baseItem, playerObj)
    local groups        = {}
    local showQty       = Options.shouldShowQuantity()
    local showPartial   = Options.shouldShowPartialIngredients()
    local sortBySpoil   = true --OPS.shouldSortBySpoilage()
    local sortByUses    = true --OPS.shouldSortByUsesRemaining()
    local showRotten    = Options.shouldShowRottenIngredientGroup()

    local itemForTexture = nil

    -- Benchmark max-use for butcher meat cuts:
    -- group by fullType + cookState, ignoring cut quality, so "Full" means "best cut available".
    local benchmarkMaxUse = {}

    for _, bucket in pairs(buckets) do
        for _, rec in ipairs(bucket.records) do
            local item = rec.item
            if item then
                -- Only for butcher meats that actually have a stamped quality token
                local q = Butchering.getMeatQuality and Butchering.getMeatQuality(item) or nil
                if q then
                    -- Apply the same poison + rotten filtering rules as group-building
                    local filteredItem = FilterPoisonAndLabel(rec, baseItem, playerObj)
                    if filteredItem then
                        local evoItem = rec.item
                        local isRotten = evoItem.isRotten and evoItem:isRotten() or false
                        if not isRotten then
                            local benchKey = rec.fullType .. "||" .. _cookToken(evoItem)
                            local u = rec.use or 0
                            local cur = benchmarkMaxUse[benchKey] or 0
                            if u > cur then
                                benchmarkMaxUse[benchKey] = u
                            end
                        end
                    end
                end
            end
        end
    end

    for _, bucket in pairs(buckets) do
        local goodRecords   = {}
        local rottenRecords = {}

        ------------------------------------------------
        -- Filter + split into GOOD vs ROTTEN
        ------------------------------------------------
        for _, rec in ipairs(bucket.records) do
            local item, extraInfo = FilterPoisonAndLabel(rec, baseItem, playerObj)
            if item then
                rec.extraInfoBase = extraInfo

                local evoItem = rec.item
                local isRotten = evoItem.isRotten and evoItem:isRotten() or false

                if isRotten then
                    table.insert(rottenRecords, rec)
                else
                    table.insert(goodRecords, rec)
                end
            end
        end

        ------------------------------------------------
        -- GOOD side: FULL + PARTIAL
        ------------------------------------------------
        if #goodRecords > 0 then
            -- Compute per-bucket maxUse first
            local bucketMaxUse = 0
            for _, rec in ipairs(goodRecords) do
                local u = rec.use or 0
                if u > bucketMaxUse then
                    bucketMaxUse = u
                end
            end

            -- Baseline maxUse: for butcher cuts, compare against the best cut (fullType + cookState).
            local maxUse = bucketMaxUse
            local treatMainAsPartial = false

            if Options.shouldSeparateIngredientQualities() then
                local anyQuality = false
                local benchKey = nil

                for _, rec in ipairs(goodRecords) do
                    local q = Butchering.getMeatQuality and Butchering.getMeatQuality(rec.item) or nil
                    if q then
                        anyQuality = true
                        benchKey = rec.fullType .. "||" .. _cookToken(rec.item)
                        break
                    end
                end

                if anyQuality and benchKey then
                    local benchMax = benchmarkMaxUse[benchKey]
                    if benchMax and benchMax > 0 then
                        maxUse = benchMax

                        -- If this cut can never reach baseline, we'll still show it as the main group,
                        -- but label it Partial (avoids disappearing when showPartial is disabled).
                        if bucketMaxUse < maxUse then
                            treatMainAsPartial = true
                        end
                    end
                end
            end

            local fullList    = {}
            local partialList = {}

            for _, rec in ipairs(goodRecords) do
                if rec.use == maxUse then
                    table.insert(fullList, rec)
                else
                    table.insert(partialList, rec)
                end
            end

            -- If no one matches baseline:
            -- - normal buckets: treat all as full (keeps vanilla-ish behaviour)
            -- - butcher cut buckets below baseline: keep all as "main" but label Partial
            if #fullList == 0 then
                fullList    = goodRecords
                partialList = {}
                -- treatMainAsPartial remains whatever we computed (true for lower cuts)
            end

            local fullCount    = #fullList
            local partialCount = #partialList
            local totalGood    = fullCount + partialCount

            ------------------------------------------------
            -- Main GOOD (full) group
            ------------------------------------------------
            if fullCount > 0 then
                local mainRec  = nil
                local mainTime = nil
                local mainUse  = nil

                for _, rec in ipairs(fullList) do
                    local t = rec.timeToRot
                    local u = rec.use or 0
                    if not mainRec then
                        mainRec  = rec
                        mainTime = t
                        mainUse  = u
                    else
                        local better = false

                        if sortBySpoil and t ~= mainTime then
                            -- Spoilage primary
                            better = (t < mainTime)
                        elseif sortByUses and u ~= mainUse then
                            -- Uses as tiebreaker (least uses first)
                            better = (u < mainUse)
                        else
                            -- Final tie-break: Poor > Medium > Prime (only when both are butcher cuts)
                            local ar = _qualityRankFromItem(rec.item)
                            local br = _qualityRankFromItem(mainRec.item)
                            if ar ~= 0 and br ~= 0 and ar ~= br then
                                better = (ar > br)
                            end
                        end

                        if better then
                            mainRec  = rec
                            mainTime = t
                            mainUse  = u
                        end
                    end
                end

                if mainRec then
                    local extraInfo = mainRec.extraInfoBase or ""

                    if treatMainAsPartial then
                        local partialText = getTextOrNull("UI_BetterCooking_Partial") or "Partial"
                        if extraInfo == "" then
                            extraInfo = partialText
                        else
                            extraInfo = extraInfo .. " " .. partialText
                        end
                    end

                    -- Quantity logic: choose count first
                    local qtyCount = totalGood
                    if showPartial then
                        qtyCount = fullCount
                    end

                    extraInfo = AppendUseAndQuantity(extraInfo, mainUse, qtyCount, showQty)

                    table.insert(groups, MakeGroup(
                        bucket,
                        mainRec,
                        mainTime,
                        mainUse,
                        qtyCount,
                        extraInfo,
                        treatMainAsPartial and "partial" or "main"
                    ))

                    -- Prefer GOOD full as icon candidate
                    if not itemForTexture and mainRec.item then
                        itemForTexture = mainRec.item
                    end
                end
            end

            ------------------------------------------------
            -- GOOD partial group (optional)
            ------------------------------------------------
            if showPartial and partialCount > 0 then
                local partialBest = nil
                local partialTime = nil
                local partialUse  = nil

                for _, rec in ipairs(partialList) do
                    local t = rec.timeToRot
                    local u = rec.use or 0

                    if not partialBest then
                        partialBest = rec
                        partialTime = t
                        partialUse  = u
                    else
                        local better = false

                        -- Spoilage-first, then least uses
                        if sortBySpoil and t ~= partialTime then
                            better = (t < partialTime)
                        elseif sortByUses and u ~= partialUse then
                            better = (u < partialUse)
                        else
                            local ar = _qualityRankFromItem(rec.item)
                            local br = _qualityRankFromItem(partialBest.item)
                            if ar ~= 0 and br ~= 0 and ar ~= br then
                                better = (ar > br)
                            end
                        end

                        if better then
                            partialBest = rec
                            partialTime = t
                            partialUse  = u
                        end
                    end
                end

                if partialBest then
                    local extraInfo = partialBest.extraInfoBase or ""
                    local partialText = getTextOrNull("UI_BetterCooking_Partial") or "Partial"

                    if extraInfo == "" then
                        extraInfo = partialText
                    else
                        extraInfo = extraInfo .. " " .. partialText
                    end

                    extraInfo = AppendUseAndQuantity(extraInfo, partialUse, partialCount, showQty)

                    table.insert(groups, MakeGroup(
                        bucket,
                        partialBest,
                        partialTime,
                        partialUse,
                        partialCount,
                        extraInfo,
                        "partial"  -- good partial ingredient
                    ))

                    -- Fallback: use partial if no full candidate exists
                    if not itemForTexture and partialBest.item then
                        itemForTexture = partialBest.item
                    end
                end
            end
        end

        ------------------------------------------------
        -- ROTTEN side: single consolidated group
        ------------------------------------------------
        if showRotten and #rottenRecords > 0 then
            local bestRec  = nil
            local bestTime = nil
            local bestUse  = nil
            local count    = #rottenRecords

            for _, rec in ipairs(rottenRecords) do
                local t = rec.timeToRot
                local u = rec.use or 0

                if not bestRec then
                    bestRec  = rec
                    bestTime = t
                    bestUse  = u
                else
                    local better = false

                    -- Rotten: primary = HIGHEST use, tiebreaker = earliest rot
                    if u > bestUse then
                        better = true
                    elseif u == bestUse and sortBySpoil and t < bestTime then
                        better = true
                    end

                    if better then
                        bestRec  = rec
                        bestTime = t
                        bestUse  = u
                    end
                end
            end

            if bestRec then
                local extraInfo = bestRec.extraInfoBase or ""
                extraInfo = AppendUseAndQuantity(extraInfo, bestUse, count, showQty)

                table.insert(groups, MakeGroup(
                    bucket,
                    bestRec,
                    bestTime,
                    bestUse,
                    count,
                    extraInfo,
                    "rotten"
                ))

                -- Last resort: rotten as icon, only if nothing else
                if not itemForTexture and bestRec.item then
                    itemForTexture = bestRec.item
                end
            end
        end
    end

    return groups, itemForTexture
end

---------------------------------------------------------------
-- Helper: sort groups according to global options
---------------------------------------------------------------

local function SortGroupsAlphabetical(groups)
    table.sort(groups, function(a, b)
        -- Prefer cooked over raw when both entries are the same base fullType
        local aRec = a.bestRecord
        local bRec = b.bestRecord
        local aFT  = aRec and aRec.fullType or nil
        local bFT  = bRec and bRec.fullType or nil

        if aFT and bFT and aFT == bFT then
            local aItem = aRec.item
            local bItem = bRec.item
            local aCooked = (aItem and aItem.isCooked and aItem:isCooked()) and 1 or 0
            local bCooked = (bItem and bItem.isCooked and bItem:isCooked()) and 1 or 0

            if aCooked ~= bCooked then
                return aCooked > bCooked
            end
        end

        local aLabel = tostring(a.label or "")
        local bLabel = tostring(b.label or "")

        -- 1) Alphabetical by label (type name)
        if aLabel ~= bLabel then
            return aLabel < bLabel
        end

        -- 2) Within same label, order by "kind":
        --    main -> partial -> rotten
        local order = { main = 3, partial = 2, rotten = 1 }
        local aKind = a.kind or "main"
        local bKind = b.kind or "main"
        local aRank = order[aKind] or 0
        local bRank = order[bKind] or 0

        if aRank ~= bRank then
            return aRank > bRank
        end

        return false
    end)
end

local function SortGroupsFirstToRot(groups, leastUses)
    table.sort(groups, function(a, b)
        -- 1) Keep kind separation stable: main -> partial -> rotten
        local order = { main = 3, partial = 2, rotten = 1 }
        local aKind = a.kind or "main"
        local bKind = b.kind or "main"
        local aRank = order[aKind] or 0
        local bRank = order[bKind] or 0

        if aRank ~= bRank then
            return aRank > bRank
        end

        -- 2) Within the same base fullType, prefer cooked over raw
        local aRec = a.bestRecord
        local bRec = b.bestRecord
        local aFT  = aRec and aRec.fullType or nil
        local bFT  = bRec and bRec.fullType or nil

        if aFT and bFT and aFT == bFT then
            local aItem = aRec.item
            local bItem = bRec.item
            local aCooked = (aItem and aItem.isCooked and aItem:isCooked()) and 1 or 0
            local bCooked = (bItem and bItem.isCooked and bItem:isCooked()) and 1 or 0

            if aCooked ~= bCooked then
                return aCooked > bCooked
            end
        end

        -- 3) Meat-cut tie-break (Poor > Medium/Average > Prime) when truly tied
        local aRec = a.bestRecord
        local bRec = b.bestRecord
        if aRec and bRec and aRec.fullType and bRec.fullType and aRec.fullType == bRec.fullType then
            local ar = _qualityRankFromItem(aRec.item)
            local br = _qualityRankFromItem(bRec.item)
            if ar ~= 0 and br ~= 0 and ar ~= br then
                return ar > br
            end
        end

        -- 4) Spoilage first (earliest timeToRot first)
        local at = a.timeToRot and tonumber(a.timeToRot) or 9999999
        local bt = b.timeToRot and tonumber(b.timeToRot) or 9999999
        if at ~= bt then
            return at < bt
        end

        -- 5) Uses remaining (most first)
        local au = tonumber(a.use) or 0
        local bu = tonumber(b.use) or 0
        if au ~= bu then
            if leastUses then
                return au < bu
            end
            return au > bu
        end

        -- 6) Tie-break by label so ordering is deterministic
        local al = tostring(a.label or "")
        local bl = tostring(b.label or "")
        if al ~= bl then
            return al < bl
        end

        return false
    end)
end

---------------------------------------------------------------
-- Helper: inject Seasonings groups into the menu
---------------------------------------------------------------

local function AddSeasoningsToMenu(context, subMenuRecipe, evorecipe2, baseItem, player, groups, itemForTexture)
    if #groups == 0 then return end

    if Options.shouldForceFirstToRotSeasonings() then
        SortGroupsFirstToRot(groups, true)
    else
        SortGroupsAlphabetical(groups)
    end

    local useSubMenu = Options.shouldUseSeasoningsSubMenu()
    local parentMenu = subMenuRecipe
    local seasoningsSubMenu = nil

    if useSubMenu then
        local opt = subMenuRecipe:addOption(getTextOrNull("UI_BetterCooking_Seasonings") or "Seasonings", nil)
        opt.iconTexture = SEASONING_ICON
        --[[
        if itemForTexture then
            opt.itemForTexture = itemForTexture
        end
        ]]
        -- Must create submenus from the root context, passing the *parent* menu.
        seasoningsSubMenu = context:getNew(subMenuRecipe)
        subMenuRecipe:addSubMenu(opt, seasoningsSubMenu)
        parentMenu = seasoningsSubMenu
    end

    for _, g in ipairs(groups) do
        local rec = g.bestRecord
        local evoItem = rec.item
        local extraInfo = g.extraInfo or ""

        -- Let vanilla build "<ItemName> <extraInfo>" and handle icons/tooltip.
        ISInventoryPaneContextMenu.addItemInEvoRecipe(
            parentMenu,
            baseItem,
            evoItem,
            extraInfo,
            evorecipe2,
            player
        )
    end
end

---------------------------------------------------------------
-- Helper: inject Ingredient groups into the menu
---------------------------------------------------------------

local function AddIngredientsToMenu(context, subMenuRecipe, evorecipe2, baseItem, player, groups, itemForTexture)
    if #groups == 0 then return end

    if Options.shouldForceFirstToRotIngredients() then
        SortGroupsFirstToRot(groups)
    else
        SortGroupsAlphabetical(groups)
    end

    local useSubMenu = Options.shouldUseIngredientsSubMenu()
    local parentMenu = subMenuRecipe
    local ingredientsSubMenu = nil

    if useSubMenu then
        local opt = subMenuRecipe:addOption(getTextOrNull("UI_BetterCooking_Ingredients") or "Ingredients", nil)
        opt.iconTexture = INGREDIENT_ICON
        --[[
        if itemForTexture then
            opt.itemForTexture = itemForTexture
        end
        ]]
        -- Same fix: root context builds the submenu, parent is subMenuRecipe.
        ingredientsSubMenu = context:getNew(subMenuRecipe)
        subMenuRecipe:addSubMenu(opt, ingredientsSubMenu)
        parentMenu = ingredientsSubMenu
    end

    for _, g in ipairs(groups) do
        local rec = g.bestRecord
        local evoItem = rec.item
        local extraInfo = g.extraInfo or ""

        ISInventoryPaneContextMenu.addItemInEvoRecipe(
            parentMenu,
            baseItem,
            evoItem,
            extraInfo,
            evorecipe2,
            player
        )
    end
end

local vanilla_doEvorecipeMenu = ISInventoryPaneContextMenu.doEvorecipeMenu
---------------------------------------------------------------
-- Override: ISInventoryPaneContextMenu.doEvorecipeMenu
--
-- This fully replaces vanilla's evolved recipe menu building,
-- but preserves the "Add Random [FoodType]" logic.
---------------------------------------------------------------
local function OnGameStart()
    ISInventoryPaneContextMenu.doEvorecipeMenu = function(context, items, player, evorecipe, baseItem, containerList)
        local playerObj = getSpecificPlayer(player)
        if not playerObj then
            Helpers.log("No playerObj; falling back to vanilla doEvorecipeMenu")
            return vanilla_doEvorecipeMenu(context, items, player, evorecipe, baseItem, containerList)
        end

        for i = 0, evorecipe:size() - 1 do
            local evorecipe2 = evorecipe:get(i)
            if not evorecipe2 then
                break
            end

            -- First pass: build buckets (seasonings & ingredients) from raw candidates
            local useableItems = evorecipe2:getItemsCanBeUse(playerObj, baseItem, containerList)
            local cookingLvl = playerObj:getPerkLevel(Perks.Cooking)
            local buckets = BuildBuckets(evorecipe2, baseItem, playerObj, cookingLvl, useableItems)
            if not buckets then
                break
            end

            -- Categories for "Add Random [FoodType]" (vanilla logic)
            local catList = ISInventoryPaneContextMenu.getEvoItemCategories(
                useableItems,
                evorecipe2
            )

            -- Recipe submenu root
            local subOption = nil
            local fromName = getText("ContextMenu_EvolvedRecipe_" .. evorecipe2:getUntranslatedName())
            if evorecipe2:isResultItem(baseItem) then
                subOption = context:addOptionOnTop(fromName, nil)
            else
                local createName = getText("ContextMenu_Create_From_Ingredient", fromName)
                subOption = context:addOptionOnTop(createName, nil)
            end

            local subMenuRecipe = context:getNew(context)
            context:addSubMenu(subOption, subMenuRecipe)

            -------------------------------------------------------
            -- 1) Add Random [FoodType] entries (unchanged)
            -------------------------------------------------------

            for catName, list in pairs(catList) do
                if getText("ContextMenu_FoodType_" .. catName) ~= "ContextMenu_FoodType_" .. catName then
                    local foodTypeName = getText("ContextMenu_FoodType_" .. catName)
                    local txt = getText("ContextMenu_FromRandom", foodTypeName)
                    if evorecipe2:isResultItem(baseItem) then
                        txt = getText("ContextMenu_AddRandom", foodTypeName)
                    end
                    local randomIndex = ZombRand(1, #list + 1)
                    subMenuRecipe:addOption(
                        txt,
                        evorecipe2,
                        ISInventoryPaneContextMenu.onAddItemInEvoRecipe,
                        baseItem,
                        list[randomIndex],
                        player
                    )
                end
            end

            -------------------------------------------------------
            -- 2) Build & inject Ingredients
            -------------------------------------------------------

            local ingredientGroups, ingredientIconItem = BuildIngredientGroups(buckets.ingredients, baseItem, playerObj)
            AddIngredientsToMenu(context, subMenuRecipe, evorecipe2, baseItem, player, ingredientGroups, ingredientIconItem)

            -------------------------------------------------------
            -- 3) Build & inject Seasonings
            -------------------------------------------------------

            local seasoningGroups, seasoningIconItem = BuildSeasoningGroups(buckets.seasonings, baseItem, playerObj)
            AddSeasoningsToMenu(context, subMenuRecipe, evorecipe2, baseItem, player, seasoningGroups, seasoningIconItem)
        end
    end
end

Events.OnGameStart.Add(OnGameStart)