local addonName, SinsToolkit = ...

SinsToolkit.GearScanner = CreateFrame("Frame")
local GearScanner = SinsToolkit.GearScanner

local itemScoreCache = {}
local cachedStatWeights = nil
local isBagScanQueued = false
local scanAfterCombat = false
local manualScanActive = false
local manualScanEquips = 0
local manualScanPending = 0
local manualScanCollecting = false
local manualScanAfterCombat = false
local GetDetailedItemLevelInfo = (C_Item and C_Item.GetDetailedItemLevelInfo) or GetDetailedItemLevelInfo
local gearTooltipsHooked = false
local pendingLootRolls = {}

local ITEM_LEVEL_SCORE_WEIGHT = 6
local QUALITY_SCORE_BONUS = {
    [2] = 20,  -- Uncommon
    [3] = 60,  -- Rare
    [4] = 120, -- Epic
    [5] = 180, -- Legendary
}

local SPEC_KEYS = {
    DEATHKNIGHT  = { "blood", "frost", "unholy" },
    DEMONHUNTER  = { "havoc", "vengeance" },
    DRUID        = { "balance", "feral", "guardian", "restoration" },
    EVOKER       = { "devastation", "preservation", "augmentation" },
    HUNTER       = { "beast-mastery", "marksmanship", "survival" },
    MAGE         = { "arcane", "fire", "frost" },
    MONK         = { "brewmaster", "mistweaver", "windwalker" },
    PALADIN      = { "holy", "protection", "retribution" },
    PRIEST       = { "discipline", "holy", "shadow" },
    ROGUE        = { "assassination", "outlaw", "subtlety" },
    SHAMAN       = { "elemental", "enhancement", "restoration" },
    WARLOCK      = { "affliction", "demonology", "destruction" },
    WARRIOR      = { "arms", "fury", "protection" },
}

local STAT_MAPPING = {
    ["Strength"]        = "ITEM_MOD_STRENGTH_SHORT",
    ["Agility"]         = "ITEM_MOD_AGILITY_SHORT",
    ["Intellect"]       = "ITEM_MOD_INTELLECT_SHORT",
    ["Stamina"]         = "ITEM_MOD_STAMINA_SHORT",
    ["Critical Strike"] = "ITEM_MOD_CRIT_RATING_SHORT",
    ["Haste"]           = "ITEM_MOD_HASTE_RATING_SHORT",
    ["Mastery"]         = "ITEM_MOD_MASTERY_RATING_SHORT",
    ["Versatility"]     = "ITEM_MOD_VERSATILITY",
}

local PRIMARY_STAT_BY_SPEC = {
    DEATHKNIGHT  = { "ITEM_MOD_STRENGTH_SHORT", "ITEM_MOD_STRENGTH_SHORT", "ITEM_MOD_STRENGTH_SHORT" },
    DEMONHUNTER  = { "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_AGILITY_SHORT" },
    DRUID        = { "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_INTELLECT_SHORT" },
    EVOKER       = { "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT" },
    HUNTER       = { "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_AGILITY_SHORT" },
    MAGE         = { "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT" },
    MONK         = { "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_AGILITY_SHORT" },
    PALADIN      = { "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_STRENGTH_SHORT", "ITEM_MOD_STRENGTH_SHORT" },
    PRIEST       = { "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT" },
    ROGUE        = { "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_AGILITY_SHORT" },
    SHAMAN       = { "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_AGILITY_SHORT", "ITEM_MOD_INTELLECT_SHORT" },
    WARLOCK      = { "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT", "ITEM_MOD_INTELLECT_SHORT" },
    WARRIOR      = { "ITEM_MOD_STRENGTH_SHORT", "ITEM_MOD_STRENGTH_SHORT", "ITEM_MOD_STRENGTH_SHORT" },
}

local equipSlotMap = {
    ["INVTYPE_HEAD"]           = { INVSLOT_HEAD },
    ["INVTYPE_NECK"]           = { INVSLOT_NECK },
    ["INVTYPE_SHOULDER"]       = { INVSLOT_SHOULDER },
    ["INVTYPE_CHEST"]          = { INVSLOT_CHEST },
    ["INVTYPE_ROBE"]           = { INVSLOT_CHEST },
    ["INVTYPE_WAIST"]          = { INVSLOT_WAIST },
    ["INVTYPE_LEGS"]           = { INVSLOT_LEGS },
    ["INVTYPE_FEET"]           = { INVSLOT_FEET },
    ["INVTYPE_WRIST"]          = { INVSLOT_WRIST },
    ["INVTYPE_HAND"]           = { INVSLOT_HAND },
    ["INVTYPE_CLOAK"]          = { INVSLOT_BACK },
    ["INVTYPE_FINGER"]         = { INVSLOT_FINGER1, INVSLOT_FINGER2 },
    ["INVTYPE_TRINKET"]        = { INVSLOT_TRINKET1, INVSLOT_TRINKET2 },
    ["INVTYPE_WEAPON"]         = { INVSLOT_MAINHAND, INVSLOT_OFFHAND },
    ["INVTYPE_2HWEAPON"]       = { INVSLOT_MAINHAND },
    ["INVTYPE_WEAPONMAINHAND"] = { INVSLOT_MAINHAND },
    ["INVTYPE_WEAPONOFFHAND"]  = { INVSLOT_OFFHAND },
    ["INVTYPE_SHIELD"]         = { INVSLOT_OFFHAND },
    ["INVTYPE_HOLDABLE"]       = { INVSLOT_OFFHAND },
}

local armorEquipLocations = {
    INVTYPE_HEAD = true,
    INVTYPE_SHOULDER = true,
    INVTYPE_CHEST = true,
    INVTYPE_ROBE = true,
    INVTYPE_WAIST = true,
    INVTYPE_LEGS = true,
    INVTYPE_FEET = true,
    INVTYPE_WRIST = true,
    INVTYPE_HAND = true,
}

local armorClassID = (Enum and Enum.ItemClass and Enum.ItemClass.Armor) or LE_ITEM_CLASS_ARMOR or 4
local armorSubclassNames = {
    [1] = "cloth",
    [2] = "leather",
    [3] = "mail",
    [4] = "plate",
}

local preferredArmorSubclassByClass = {
    DEATHKNIGHT = 4,
    DEMONHUNTER = 2,
    DRUID = 2,
    EVOKER = 3,
    HUNTER = 3,
    MAGE = 1,
    MONK = 2,
    PALADIN = 4,
    PRIEST = 1,
    ROGUE = 2,
    SHAMAN = 3,
    WARLOCK = 1,
    WARRIOR = 4,
}

local SLOT_LABELS = {
    [INVSLOT_HEAD] = HEADSLOT,
    [INVSLOT_NECK] = NECKSLOT,
    [INVSLOT_SHOULDER] = SHOULDERSLOT,
    [INVSLOT_BACK] = BACKSLOT,
    [INVSLOT_CHEST] = CHESTSLOT,
    [INVSLOT_WRIST] = WRISTSLOT,
    [INVSLOT_HAND] = HANDSSLOT,
    [INVSLOT_WAIST] = WAISTSLOT,
    [INVSLOT_LEGS] = LEGSSLOT,
    [INVSLOT_FEET] = FEETSLOT,
    [INVSLOT_FINGER1] = FINGER0SLOT,
    [INVSLOT_FINGER2] = FINGER1SLOT,
    [INVSLOT_TRINKET1] = TRINKET0SLOT,
    [INVSLOT_TRINKET2] = TRINKET1SLOT,
    [INVSLOT_MAINHAND] = MAINHANDSLOT,
    [INVSLOT_OFFHAND] = SECONDARYHANDSLOT,
}

local function Print(message)
    print("|cff6f2da8[Sin's Toolkit]|r " .. message)
end

local function GetItemEquipLocation(itemLink)
    if not itemLink then return nil end
    if not C_Item or not C_Item.GetItemInfo then return nil end
    return select(9, C_Item.GetItemInfo(itemLink))
end

local function GetInstantEquipLocation(itemLink)
    if not itemLink or not GetItemInfoInstant then return nil end
    return select(4, GetItemInfoInstant(itemLink))
end

local function GetItemLevel(itemLink)
    if not itemLink then return 0 end

    if GetDetailedItemLevelInfo then
        local ok, itemLevel = pcall(GetDetailedItemLevelInfo, itemLink)
        if ok and itemLevel then
            return itemLevel
        end
    end

    if C_Item and C_Item.GetItemInfo then
        return select(4, C_Item.GetItemInfo(itemLink)) or 0
    end

    return 0
end

local function GetItemQuality(itemLink)
    if not itemLink then return 0 end
    if C_Item and C_Item.GetItemInfo then
        local ok, quality = pcall(function()
            return select(3, C_Item.GetItemInfo(itemLink))
        end)
        if ok and quality then return quality end
    end
    return 0
end

local function GetItemLevelScore(itemLink)
    return GetItemLevel(itemLink) * ITEM_LEVEL_SCORE_WEIGHT
end

local function GetItemQualityScore(itemLink)
    return QUALITY_SCORE_BONUS[GetItemQuality(itemLink)] or 0
end

local function IsItemUsableLevel(itemLink)
    if not itemLink or not C_Item or not C_Item.GetItemInfo then return true end
    local ok, minLevel = pcall(function()
        return select(5, C_Item.GetItemInfo(itemLink))
    end)
    if ok and minLevel and minLevel > 0 and UnitLevel("player") < minLevel then
        return false, ("Requires level %d."):format(minLevel)
    end
    return true
end

local function GetItemArmorTypeFailureReason(itemLink)
    if not itemLink or not GetItemInfoInstant then return nil end

    local _, _, itemSubType, itemEquipLoc, _, itemClassID, itemSubClassID = GetItemInfoInstant(itemLink)
    if itemClassID ~= armorClassID or not armorEquipLocations[itemEquipLoc] then
        return nil
    end

    local _, classToken = UnitClass("player")
    local preferredSubclass = preferredArmorSubclassByClass[classToken]
    if not preferredSubclass or itemSubClassID == preferredSubclass then
        return nil
    end

    local preferredName = armorSubclassNames[preferredSubclass] or "your class armor type"
    local itemArmorName = itemSubType and itemSubType:lower() or armorSubclassNames[itemSubClassID] or "this armor type"
    return ("Your class uses %s armor for this slot; this item is %s."):format(preferredName, itemArmorName)
end

local function IsItemEquippableByPlayer(itemLink)
    if not itemLink then return false end

    local levelOk = IsItemUsableLevel(itemLink)
    if not levelOk then
        return false
    end

    if GetItemArmorTypeFailureReason(itemLink) then
        return false
    end

    if C_Item and C_Item.IsEquippableItem then
        local ok, equippable = pcall(C_Item.IsEquippableItem, itemLink)
        if ok and not equippable then return false end
    end

    if IsEquippableItem then
        local ok, equippable = pcall(IsEquippableItem, itemLink)
        if ok and not equippable then return false end
    end

    return true
end

local function IsPawnAvailable()
    return PawnShouldItemLinkHaveUpgradeArrow or (PawnGetItemData and PawnIsItemAnUpgrade)
end

function SinsToolkit.IsPawnAvailable()
    return IsPawnAvailable()
end

local function IsGearScannerEnabled()
    if SinsToolkit.IsGearScannerProviderAvailable and not SinsToolkit.IsGearScannerProviderAvailable() then return false end
    return SinsToolkit.IsFeatureActive and SinsToolkit.IsFeatureActive("gearScannerEnabled")
end

local function IsGearSlotLocked(slotID)
    return SinsToolkitDB
        and SinsToolkitDB.gearScannerLockSlots
        and SinsToolkitDB.gearScannerLockedSlots
        and SinsToolkitDB.gearScannerLockedSlots[slotID] == true
end

local function GetMinimumUpgradeScore()
    return (SinsToolkitDB and tonumber(SinsToolkitDB.gearScannerMinimumUpgrade)) or 0
end

local function GetRequiredUpgradeDelta(currentScore)
    local configured = GetMinimumUpgradeScore()
    local pawnLikeDelta = currentScore and currentScore > 0 and (currentScore * 0.005) or 0
    return math.max(configured, pawnLikeDelta)
end

local function CheckMaxLevel()
    if not IsGearScannerEnabled() then return end
    if not SinsToolkitDB or SinsToolkitDB.gearScannerMaxLevelDisable == false then return end

    local maxLevel = (GetMaxLevelForPlayerExpansion and GetMaxLevelForPlayerExpansion())
        or (GetMaxPlayerLevel and GetMaxPlayerLevel())
        or 80

    if UnitLevel("player") >= maxLevel and SinsToolkitDB.gearScannerEnabled then
        SinsToolkitDB.gearScannerEnabled = false
        if not SinsToolkitDB.gearScannerSilent then
            Print("Gear Scanner disabled at level " .. maxLevel .. ". Manual optimization is recommended at max level.")
        end
    end
end

-- Extract the active Hero Talent to ensure we fetch the right priority stats
local function GetActiveHeroTalentName()
    if not C_ClassTalents or not C_ClassTalents.GetActiveHeroTalentSpec then return nil end
    local subTreeID = C_ClassTalents.GetActiveHeroTalentSpec()
    if not subTreeID or subTreeID == 0 then return nil end
    if C_Traits and C_Traits.GetSubTreeInfo then
        local ok, info = pcall(C_Traits.GetSubTreeInfo, subTreeID)
        if not ok and C_ClassTalents.GetActiveConfigID then
            local configID = C_ClassTalents.GetActiveConfigID()
            if configID then
                ok, info = pcall(C_Traits.GetSubTreeInfo, configID, subTreeID)
            end
        end
        if ok and info and info.name then
            return info.name
        end
    end
    return nil
end

local function PriorityMatches(priority, heroTalent, statContext)
    -- Use partial string matches so things like 'Templar' and 'Templar ES' match correctly
    if heroTalent and heroTalent ~= "All" and priority.heroTalent ~= "All" then
        if priority.heroTalent ~= heroTalent and 
           not string.find(heroTalent, priority.heroTalent, 1, true) and 
           not string.find(priority.heroTalent, heroTalent, 1, true) then
            return false
        end
    end
    if statContext and priority.context ~= statContext and priority.context ~= "General" then
        return false
    end
    return true
end

local function PickClassCodexPriority(specData, specKey)
    if not specData or not specData.priorities then return nil end

    local saved = ClassCodexCharDB and ClassCodexCharDB.perSpec and ClassCodexCharDB.perSpec[specKey]
    local heroTalent = (saved and saved.heroTalent) or GetActiveHeroTalentName()
    local statContext = saved and saved.statContext

    if heroTalent or statContext then
        for _, priority in ipairs(specData.priorities) do
            if PriorityMatches(priority, heroTalent, statContext) then
                return priority
            end
        end
    end

    return specData.priorities[1]
end

local function GetActiveStatWeights()
    if cachedStatWeights then return cachedStatWeights end
    if not ClassCodexData then return nil end

    local _, classToken = UnitClass("player")
    local specIndex = GetSpecialization and GetSpecialization()
    if not specIndex or not SPEC_KEYS[classToken] then return nil end

    local specKey = SPEC_KEYS[classToken][specIndex]
    local specData = ClassCodexData[classToken] and ClassCodexData[classToken][specKey]
    local priority = PickClassCodexPriority(specData, specKey)
    local priorityList = priority and priority.stats
    
    if not priorityList then return nil end -- Bail if no ClassCodex logic exists for this spec

    local weights = {}
    local baseWeight = 100
    local weightDegradation = 20
    
    -- 1. Apply descending weights to secondary stats directly from Class Codex priorities
    for tierIndex, statGroup in ipairs(priorityList) do
        local currentWeight = math.max(10, baseWeight - ((tierIndex - 1) * weightDegradation))
        for _, statName in ipairs(statGroup) do
            local apiStatKey = STAT_MAPPING[statName]
            if apiStatKey then
                weights[apiStatKey] = currentWeight
            end
        end
    end

    -- 2. Determine and apply a heavy weight to the character's primary stat
    local primaryStatKey
    if GetSpecializationInfo then
        local _, _, _, _, _, primaryStatIndex = GetSpecializationInfo(specIndex)
        if primaryStatIndex == 1 then primaryStatKey = "ITEM_MOD_STRENGTH_SHORT"
        elseif primaryStatIndex == 2 then primaryStatKey = "ITEM_MOD_AGILITY_SHORT"
        elseif primaryStatIndex == 4 then primaryStatKey = "ITEM_MOD_INTELLECT_SHORT" end
    end
    
    -- Fallback map for primary stats
    if not primaryStatKey then
        primaryStatKey = PRIMARY_STAT_BY_SPEC[classToken] and PRIMARY_STAT_BY_SPEC[classToken][specIndex]
    end
    primaryStatKey = primaryStatKey or "ITEM_MOD_INTELLECT_SHORT"

    weights[primaryStatKey] = 150

    if primaryStatKey == "ITEM_MOD_AGILITY_SHORT" or primaryStatKey == "ITEM_MOD_STRENGTH_SHORT" then
        weights["ITEM_MOD_DAMAGE_PER_SECOND_SHORT"] = 200 -- Value weapon DPS very highly for physical DPS
    end

    cachedStatWeights = weights
    return weights
end

local function GetPawnUpgradeScore(itemLink)
    if not SinsToolkitDB or not SinsToolkitDB.gearScannerUsePawn or not IsPawnAvailable() then return nil end

    if PawnShouldItemLinkHaveUpgradeArrow then
        local ok, isUpgrade = pcall(PawnShouldItemLinkHaveUpgradeArrow, itemLink, true)
        if ok and isUpgrade == true then
            return GetItemLevelScore(itemLink) + GetItemQualityScore(itemLink) + 100000
        end
        return nil
    end

    if PawnGetItemData and PawnIsItemAnUpgrade then
        local ok, itemData = pcall(PawnGetItemData, itemLink)
        if not ok or not itemData then return nil end

        local upgradeOk, upgradeInfo, itemLevelIncrease = pcall(PawnIsItemAnUpgrade, itemData)
        if upgradeOk and (upgradeInfo ~= nil or itemLevelIncrease ~= nil) then
            return GetItemLevelScore(itemLink) + GetItemQualityScore(itemLink) + 100000
        end
    end

    return nil
end

local function CalculateItemScore(itemLink, skipEquipCheck)
    if itemScoreCache[itemLink] then return itemScoreCache[itemLink] end

    if not skipEquipCheck and not IsItemEquippableByPlayer(itemLink) then
        itemScoreCache[itemLink] = 0
        return 0
    end

    local pawnScore = GetPawnUpgradeScore(itemLink)
    if pawnScore ~= nil then
        itemScoreCache[itemLink] = pawnScore
        return pawnScore
    end

    local weights = GetActiveStatWeights()
    if not weights then return 0 end

    local stats = (C_Item and C_Item.GetItemStats and C_Item.GetItemStats(itemLink)) or {}
    local score = GetItemLevelScore(itemLink) + GetItemQualityScore(itemLink)
    for statKey, statValue in pairs(stats) do
        if weights[statKey] then
            score = score + (statValue * weights[statKey])
        end
    end

    itemScoreCache[itemLink] = score
    return score
end

local function GetCurrentSlotScore(slotID)
    local currentItemLink = GetInventoryItemLink("player", slotID)
    return currentItemLink and CalculateItemScore(currentItemLink, true) or 0
end

local function IsDisplayableItemLink(value)
    return type(value) == "string" and value:find("|Hitem:", 1, true) ~= nil
end

local function GetPawnUpgradeExplanation(itemLink)
    if not SinsToolkitDB or not SinsToolkitDB.gearScannerUsePawn or not IsPawnAvailable() then return nil end

    if PawnGetItemData and PawnIsItemAnUpgrade then
        local ok, itemData = pcall(PawnGetItemData, itemLink)
        if ok and itemData then
            local upgradeOk, upgradeInfo, itemLevelIncrease = pcall(PawnIsItemAnUpgrade, itemData, true)
            if upgradeOk then
                if type(upgradeInfo) == "table" and upgradeInfo[1] then
                    local upgrade = upgradeInfo[1]
                    local scaleName = upgrade.LocalizedScaleName or upgrade.ScaleName
                    local percentUpgrade = tonumber(upgrade.PercentUpgrade)
                    local existingItemLink = upgrade.ExistingItemLink

                    if scaleName and percentUpgrade then
                        local upgradeText = percentUpgrade >= 100 and "major upgrade" or ("%.1f%% upgrade"):format(percentUpgrade * 100)
                        if IsDisplayableItemLink(existingItemLink) then
                            return ("Pawn: %s for %s vs %s"):format(upgradeText, scaleName, existingItemLink)
                        end
                        return ("Pawn: %s for %s"):format(upgradeText, scaleName)
                    elseif scaleName then
                        return ("Pawn: upgrade for %s"):format(scaleName)
                    end
                end

                local itemLevelDelta = tonumber(itemLevelIncrease)
                if itemLevelDelta then
                    return ("Pawn: +%.0f item levels"):format(itemLevelDelta)
                end
            end
        end
    end

    if PawnShouldItemLinkHaveUpgradeArrow then
        local ok, isUpgrade = pcall(PawnShouldItemLinkHaveUpgradeArrow, itemLink, true)
        if ok and isUpgrade == true then
            return "Pawn: upgrade advisor marked this item"
        end
    end

    return nil
end

local function GetUpgradeExplanation(itemLink, slotID, newScore, currentScore, currentItemLink)
    local pawnExplanation = GetPawnUpgradeExplanation(itemLink)
    if pawnExplanation then return pawnExplanation end

    local slotName = SLOT_LABELS[slotID] or ("slot " .. tostring(slotID))
    if currentItemLink then
        return ("ClassCodex: +%.1f score for %s vs %s"):format((newScore or 0) - (currentScore or 0), slotName, currentItemLink)
    end

    return ("ClassCodex: fills empty %s with score %.1f"):format(slotName, newScore or 0)
end

local function IsTwoHandEquipped()
    local mainHandLink = GetInventoryItemLink("player", INVSLOT_MAINHAND)
    return GetItemEquipLocation(mainHandLink) == "INVTYPE_2HWEAPON"
end

local function GetBestReplacementSlot(itemEquipLoc, targetSlots, newScore)
    if itemEquipLoc == "INVTYPE_2HWEAPON" then
        if IsGearSlotLocked(INVSLOT_MAINHAND) or IsGearSlotLocked(INVSLOT_OFFHAND) then
            return nil
        end
        local currentWeaponScore = GetCurrentSlotScore(INVSLOT_MAINHAND) + GetCurrentSlotScore(INVSLOT_OFFHAND)
        return newScore > (currentWeaponScore + GetRequiredUpgradeDelta(currentWeaponScore)) and INVSLOT_MAINHAND or nil
    end

    if itemEquipLoc == "INVTYPE_WEAPONOFFHAND"
        or itemEquipLoc == "INVTYPE_SHIELD"
        or itemEquipLoc == "INVTYPE_HOLDABLE" then
        if IsTwoHandEquipped() or IsGearSlotLocked(INVSLOT_OFFHAND) then
            return nil
        end
    end

    local bestSlotToReplace = nil
    local lowestCurrentScore = math.huge

    for _, slotID in ipairs(targetSlots) do
        if slotID ~= INVSLOT_OFFHAND or itemEquipLoc ~= "INVTYPE_WEAPON" or CanDualWield() then
            if not IsGearSlotLocked(slotID) then
                local currentScore = GetCurrentSlotScore(slotID)
                if currentScore < lowestCurrentScore then
                    lowestCurrentScore = currentScore
                    bestSlotToReplace = slotID
                end
            end
        end
    end

    if not bestSlotToReplace or newScore <= (lowestCurrentScore + GetRequiredUpgradeDelta(lowestCurrentScore)) then return nil end
    return bestSlotToReplace
end

local function FinishManualScanIfReady()
    if not manualScanActive or manualScanCollecting or manualScanPending > 0 then return end
    manualScanActive = false
    if manualScanEquips == 0 then
        Print("No upgrades found.")
    end
end

local function EvaluateAndEquipItem(bag, slot, itemLink)
    if not IsGearScannerEnabled() or InCombatLockdown() then return end

    local itemEquipLoc = GetItemEquipLocation(itemLink)
    if not itemEquipLoc or itemEquipLoc == "" then return end

    local targetSlots = equipSlotMap[itemEquipLoc]
    if not targetSlots then return end
    if not IsItemEquippableByPlayer(itemLink) then return end

    local itemLocation = ItemLocation:CreateFromBagAndSlot(bag, slot)
    if not itemLocation then return end
    if itemLocation.IsValid and not itemLocation:IsValid() then return end

    -- Keep BoE drops untouched unless the user explicitly opts into equipping unbound upgrades.
    if not SinsToolkitDB.gearScannerAllowUnbound and not C_Item.IsBound(itemLocation) then return end

    local newScore = CalculateItemScore(itemLink)
    if newScore <= 0 then return end

    local bestSlotToReplace = GetBestReplacementSlot(itemEquipLoc, targetSlots, newScore)
    if not bestSlotToReplace then return end
    local currentItemLink = GetInventoryItemLink("player", bestSlotToReplace)
    local currentScore = GetCurrentSlotScore(bestSlotToReplace)
    local explanation = GetUpgradeExplanation(itemLink, bestSlotToReplace, newScore, currentScore, currentItemLink)

    ClearCursor()
    C_Container.PickupContainerItem(bag, slot)
    EquipCursorItem(bestSlotToReplace)
    ClearCursor()
    if manualScanActive then
        manualScanEquips = manualScanEquips + 1
    end

    if not SinsToolkitDB.gearScannerSilent then
        Print("Auto-equipped upgrade: " .. itemLink .. " (" .. explanation .. ")")
    end
end

local function GetEquipStatus(itemLink)
    if not IsGearScannerEnabled() then
        return nil, "Gear Scanner is disabled."
    end

    if SinsToolkit.IsGearScannerProviderAvailable and not SinsToolkit.IsGearScannerProviderAvailable() then
        return nil, "Pawn or ClassCodex is required for upgrade evaluation."
    end

    local itemEquipLoc = GetItemEquipLocation(itemLink)
    if not itemEquipLoc or itemEquipLoc == "" then
        return nil, "This item is not equippable gear."
    end

    local targetSlots = equipSlotMap[itemEquipLoc]
    if not targetSlots then
        return nil, "This equipment slot is not handled by Gear Scanner."
    end

    local armorFailureReason = GetItemArmorTypeFailureReason(itemLink)
    if armorFailureReason then
        return nil, armorFailureReason
    end

    if not IsItemEquippableByPlayer(itemLink) then
        return nil, "Your character cannot equip this item."
    end

    local score = CalculateItemScore(itemLink)
    if score <= 0 then
        if SinsToolkitDB and SinsToolkitDB.gearScannerUsePawn and not IsPawnAvailable() then
            return score, "Pawn preference is enabled, but Pawn is not installed or loaded. ClassCodex fallback could not score this item."
        end
        return score, "No Pawn or ClassCodex-based score could be calculated."
    end

    local slotID = GetBestReplacementSlot(itemEquipLoc, targetSlots, score)
    if not slotID then
        local lockedNames = {}
        for _, targetSlot in ipairs(targetSlots) do
            if IsGearSlotLocked(targetSlot) then
                lockedNames[#lockedNames + 1] = SLOT_LABELS[targetSlot] or tostring(targetSlot)
            end
        end
        if #lockedNames > 0 then
            return score, "Relevant slot locked: " .. table.concat(lockedNames, ", ")
        end
        return score, "Not an upgrade over your currently equipped item."
    end

    return score, "Upgrade candidate for " .. (SLOT_LABELS[slotID] or "slot " .. slotID) .. "."
end

local function GetRollItemInfo(rollID)
    if not GetLootRollItemLink then return nil end
    local itemLink = GetLootRollItemLink(rollID)
    if not itemLink then return nil end

    local _, _, _, quality, bindOnPickUp, canNeed, canGreed = GetLootRollItemInfo(rollID)
    if not quality and C_Item and C_Item.GetItemInfo then
        quality = select(3, C_Item.GetItemInfo(itemLink))
    end

    local equipLoc = GetInstantEquipLocation(itemLink) or GetItemEquipLocation(itemLink)
    return {
        link = itemLink,
        quality = quality or 0,
        bindOnPickUp = bindOnPickUp == true,
        canNeed = canNeed == true,
        canGreed = canGreed == true,
        isGear = equipLoc and equipLoc ~= "" and equipSlotMap[equipLoc] ~= nil,
    }
end

local function ShouldHandleLootRoll(info)
    if not info then return false end
    if not info.isGear then
        return SinsToolkitDB.gearScannerRollNonGearLoot == true
    end
    if info.quality == 2 then
        return SinsToolkitDB.gearScannerAutoRollGreensBopBlues == true
    end
    if info.quality == 3 then
        return info.bindOnPickUp and SinsToolkitDB.gearScannerAutoRollGreensBopBlues == true
            or (not info.bindOnPickUp and SinsToolkitDB.gearScannerAutoRollBoeBlues == true)
    end
    if info.quality == 4 then
        return SinsToolkitDB.gearScannerAutoRollEpics == true
    end
    return false
end

local function HandleLootRoll(rollID)
    if not SinsToolkitDB or not IsGearScannerEnabled() then return end
    if not RollOnLoot then return end

    local info = GetRollItemInfo(rollID)
    if not ShouldHandleLootRoll(info) then return end

    local rollType
    if info.isGear then
        local _, reason = GetEquipStatus(info.link)
        local isUpgrade = reason and reason:find("Upgrade candidate", 1, true) == 1
        if isUpgrade and info.canNeed and not SinsToolkitDB.gearScannerNeverAutoNeed then
            rollType = 1
        elseif info.canGreed then
            rollType = 2
        end
    elseif info.canGreed then
        rollType = 2
    end

    if rollType then
        pendingLootRolls[rollID] = true
        RollOnLoot(rollID, rollType)
        if not SinsToolkitDB.gearScannerSilent then
            Print("Rolled " .. (rollType == 1 and "Need" or "Greed") .. " on " .. info.link .. ".")
        end
    end
end

local function ConfirmPendingLootRoll(rollID, rollType)
    if not pendingLootRolls[rollID] or not ConfirmLootRoll then return end
    pendingLootRolls[rollID] = nil
    ConfirmLootRoll(rollID, rollType)
end

local function ShouldConfirmBinding(quality)
    if not quality then return false end
    if quality == 3 then return SinsToolkitDB.gearScannerAutoConfirmBindBlues == true end
    if quality == 4 then return SinsToolkitDB.gearScannerAutoConfirmBindEpics == true end
    return SinsToolkitDB.gearScannerAutoConfirmBindOther == true
end

local function HandleBindingConfirm(slot)
    if not SinsToolkitDB or not EquipPendingItem then return end
    local quality
    if C_Cursor and C_Cursor.GetCursorItem and Item and Item.CreateFromItemLocation then
        local ok, item = pcall(function()
            return Item:CreateFromItemLocation(C_Cursor.GetCursorItem())
        end)
        if ok and item and item.GetItemQuality then
            quality = item:GetItemQuality()
        end
    end
    if ShouldConfirmBinding(quality) then
        EquipPendingItem(slot)
    end
end

function SinsToolkit.GetGearScannerItemScore(itemLink)
    return CalculateItemScore(itemLink)
end

function SinsToolkit.GetGearScannerEquipStatus(itemLink)
    return GetEquipStatus(itemLink)
end

function SinsToolkit.RefreshGearScannerScores()
    itemScoreCache = {}
    cachedStatWeights = nil
end

local function AddGearTooltipInfo(tooltip, itemLink)
    if not tooltip or not itemLink or not SinsToolkitDB then return end
    if type(itemLink) ~= "string" or not itemLink:find("item:", 1, true) then return end
    if tooltip.__sinsToolkitGearLink == itemLink then return end
    tooltip.__sinsToolkitGearLink = itemLink

    if not SinsToolkitDB.gearScannerScoreTooltips and not SinsToolkitDB.gearScannerReasonsTooltips then return end

    local score, reason = GetEquipStatus(itemLink)
    if SinsToolkitDB.gearScannerScoreTooltips and score and score > 0 then
        tooltip:AddLine(" ")
        tooltip:AddDoubleLine("Sin's Toolkit score", string.format("%.1f", score), 0.74, 0.45, 0.95, 0.85, 0.66, 0.30)
    end

    if SinsToolkitDB.gearScannerReasonsTooltips and reason then
        if not SinsToolkitDB.gearScannerScoreTooltips or not score or score <= 0 then
            tooltip:AddLine(" ")
        end
        tooltip:AddLine(reason, 0.78, 0.74, 0.84, true)
    end

    tooltip:Show()
end

local function HookGearTooltips()
    if gearTooltipsHooked then return end
    gearTooltipsHooked = true

    if GameTooltip then
        if C_Container and C_Container.GetContainerItemLink then
            hooksecurefunc(GameTooltip, "SetBagItem", function(tooltip, bag, slot)
                AddGearTooltipInfo(tooltip, C_Container.GetContainerItemLink(bag, slot))
            end)
        end
        hooksecurefunc(GameTooltip, "SetInventoryItem", function(tooltip, unit, slot)
            AddGearTooltipInfo(tooltip, GetInventoryItemLink(unit, slot))
        end)
        hooksecurefunc(GameTooltip, "SetHyperlink", function(tooltip, link)
            AddGearTooltipInfo(tooltip, link)
        end)
        GameTooltip:HookScript("OnTooltipCleared", function(tooltip)
            tooltip.__sinsToolkitGearLink = nil
        end)
    end

    if ItemRefTooltip then
        hooksecurefunc(ItemRefTooltip, "SetHyperlink", function(tooltip, link)
            AddGearTooltipInfo(tooltip, link)
        end)
        ItemRefTooltip:HookScript("OnTooltipCleared", function(tooltip)
            tooltip.__sinsToolkitGearLink = nil
        end)
    end
end

local function ShouldScanBagItem(bag, slot, itemLink)
    local itemEquipLoc = GetInstantEquipLocation(itemLink)
    if not itemEquipLoc or itemEquipLoc == "" or not equipSlotMap[itemEquipLoc] then
        return false
    end

    if not IsItemEquippableByPlayer(itemLink) then
        return false
    end

    if not SinsToolkitDB.gearScannerAllowUnbound then
        local itemLocation = ItemLocation:CreateFromBagAndSlot(bag, slot)
        if not itemLocation then return false end
        if itemLocation.IsValid and not itemLocation:IsValid() then return false end
        if not C_Item or not C_Item.IsBound or not C_Item.IsBound(itemLocation) then return false end
    end

    return true
end

local function PerformBagScan()
    isBagScanQueued = false

    if not IsGearScannerEnabled() then return end
    if not C_Container or not C_Container.GetContainerNumSlots or not C_Container.GetContainerItemLink then return end
    if InCombatLockdown() then
        scanAfterCombat = true
        return
    end

    manualScanCollecting = manualScanActive
    for bag = 0, NUM_BAG_SLOTS do
        for slot = 1, C_Container.GetContainerNumSlots(bag) do
            local itemLink = C_Container.GetContainerItemLink(bag, slot)
            if itemLink and ShouldScanBagItem(bag, slot, itemLink) then
                local item = Item:CreateFromItemLink(itemLink)
                if item and not item:IsItemEmpty() then
                    if manualScanActive then
                        manualScanPending = manualScanPending + 1
                    end
                    item:ContinueOnItemLoad(function()
                        EvaluateAndEquipItem(bag, slot, itemLink)
                        if manualScanActive then
                            manualScanPending = math.max(0, manualScanPending - 1)
                            FinishManualScanIfReady()
                        end
                    end)
                end
            end
        end
    end

    manualScanCollecting = false
    FinishManualScanIfReady()
end

local function QueueBagScan()
    if not IsGearScannerEnabled() then return end
    if SinsToolkitDB and SinsToolkitDB.gearScannerScanOnLoot == false then return end
    if isBagScanQueued then return end
    isBagScanQueued = true
    C_Timer.After((SinsToolkitDB and SinsToolkitDB.gearScannerLootDelay) or 0.5, PerformBagScan)
end

function SinsToolkit.ForceGearScan()
    if SinsToolkit.IsGearScannerProviderAvailable and not SinsToolkit.IsGearScannerProviderAvailable() then
        Print("Gear Scanner requires Pawn or ClassCodex.")
        return
    end

    if not IsGearScannerEnabled() then
        Print("Gear Scanner is disabled.")
        return
    end

    itemScoreCache = {}
    if InCombatLockdown() then
        manualScanAfterCombat = true
        Print("Gear scan queued until combat ends.")
        return
    end

    Print("Scanning bags for gear upgrades.")
    manualScanActive = true
    manualScanEquips = 0
    manualScanPending = 0
    isBagScanQueued = false
    PerformBagScan()
end

GearScanner:SetScript("OnEvent", function(_, event, ...)
    if event == "ADDON_LOADED" then
        local loadedAddon = ...
        if loadedAddon ~= addonName then return end

        SinsToolkitDB = SinsToolkitDB or {}
        if SinsToolkitDB.gearScannerEnabled == nil then SinsToolkitDB.gearScannerEnabled = true end
        if SinsToolkitDB.gearScannerMaxLevelDisable == nil then SinsToolkitDB.gearScannerMaxLevelDisable = true end
        if SinsToolkitDB.gearScannerSilent == nil then SinsToolkitDB.gearScannerSilent = false end
        if SinsToolkitDB.gearScannerLootDelay == nil then SinsToolkitDB.gearScannerLootDelay = 0.5 end
        if SinsToolkitDB.gearScannerScanOnLoot == nil then SinsToolkitDB.gearScannerScanOnLoot = true end
        if SinsToolkitDB.gearScannerAllowUnbound == nil then SinsToolkitDB.gearScannerAllowUnbound = false end
        if SinsToolkitDB.gearScannerMinimumUpgrade == nil then SinsToolkitDB.gearScannerMinimumUpgrade = 0 end
        if SinsToolkitDB.gearScannerScoreTooltips == nil then SinsToolkitDB.gearScannerScoreTooltips = true end
        if SinsToolkitDB.gearScannerReasonsTooltips == nil then SinsToolkitDB.gearScannerReasonsTooltips = false end
        if SinsToolkitDB.gearScannerLockSlots == nil then SinsToolkitDB.gearScannerLockSlots = false end
        if SinsToolkitDB.gearScannerAutoRollGreensBopBlues == nil then SinsToolkitDB.gearScannerAutoRollGreensBopBlues = false end
        if SinsToolkitDB.gearScannerAutoRollBoeBlues == nil then SinsToolkitDB.gearScannerAutoRollBoeBlues = false end
        if SinsToolkitDB.gearScannerAutoRollEpics == nil then SinsToolkitDB.gearScannerAutoRollEpics = false end
        if SinsToolkitDB.gearScannerRollNonGearLoot == nil then SinsToolkitDB.gearScannerRollNonGearLoot = false end
        if SinsToolkitDB.gearScannerNeverAutoNeed == nil then SinsToolkitDB.gearScannerNeverAutoNeed = true end
        if SinsToolkitDB.gearScannerAutoConfirmBindOther == nil then SinsToolkitDB.gearScannerAutoConfirmBindOther = false end
        if SinsToolkitDB.gearScannerAutoConfirmBindBlues == nil then SinsToolkitDB.gearScannerAutoConfirmBindBlues = false end
        if SinsToolkitDB.gearScannerAutoConfirmBindEpics == nil then SinsToolkitDB.gearScannerAutoConfirmBindEpics = false end
        if SinsToolkitDB.gearScannerUsePawn == nil then SinsToolkitDB.gearScannerUsePawn = false end
        SinsToolkitDB.gearScannerLockedSlots = SinsToolkitDB.gearScannerLockedSlots or {}
        if SinsToolkit.ApplyDependencyGuards then SinsToolkit.ApplyDependencyGuards(false) end
        HookGearTooltips()
    elseif event == "START_LOOT_ROLL" then
        HandleLootRoll(...)
    elseif event == "CONFIRM_LOOT_ROLL" or event == "CONFIRM_DISENCHANT_ROLL" then
        ConfirmPendingLootRoll(...)
    elseif event == "EQUIP_BIND_CONFIRM" or event == "EQUIP_BIND_REFUNDABLE_CONFIRM" or event == "EQUIP_BIND_TRADEABLE_CONFIRM" then
        HandleBindingConfirm(...)
    elseif event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_LEVEL_UP" then
        CheckMaxLevel()
    elseif event == "BAG_UPDATE_DELAYED" then
        QueueBagScan()
    elseif event == "PLAYER_REGEN_ENABLED" then
        if manualScanAfterCombat then
            manualScanAfterCombat = false
            SinsToolkit.ForceGearScan()
        elseif scanAfterCombat then
            scanAfterCombat = false
            QueueBagScan()
        end
    elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
        itemScoreCache = {}
        cachedStatWeights = nil
        QueueBagScan()
    end
end)

local function RegisterEventSafe(event)
    pcall(GearScanner.RegisterEvent, GearScanner, event)
end

RegisterEventSafe("ADDON_LOADED")
RegisterEventSafe("PLAYER_ENTERING_WORLD")
RegisterEventSafe("PLAYER_LEVEL_UP")
RegisterEventSafe("BAG_UPDATE_DELAYED")
RegisterEventSafe("PLAYER_REGEN_ENABLED")
RegisterEventSafe("PLAYER_SPECIALIZATION_CHANGED")
RegisterEventSafe("START_LOOT_ROLL")
RegisterEventSafe("CONFIRM_LOOT_ROLL")
RegisterEventSafe("CONFIRM_DISENCHANT_ROLL")
RegisterEventSafe("EQUIP_BIND_CONFIRM")
RegisterEventSafe("EQUIP_BIND_REFUNDABLE_CONFIRM")
RegisterEventSafe("EQUIP_BIND_TRADEABLE_CONFIRM")

