local _, ns = ...

local function IsSecret(v)
    return issecretvalue and issecretvalue(v) or false
end

local BWV2 = {}
ns.BWV2 = BWV2

BWV2.raidResults = {}
BWV2.missingByPlayer = {}
BWV2.activeWatchers = {}
BWV2.inventoryStatus = {}
BWV2.scanInProgress = false
BWV2.lastScanTime = 0
BWV2.isDead = false

BWV2.buffSnapshot = {}
BWV2.buffDropReminded = {}
BWV2.classBuffSelfCache = {}
BWV2.classBuffInstanceIDs = {}

BWV2.scanResults = {
    raidBuffs = {},
    consumables = {},
    inventory = {},
    classBuffs = {},
}

function BWV2:ResetState()
    wipe(self.raidResults)
    wipe(self.missingByPlayer)
    wipe(self.inventoryStatus)
    if not self.scanResults then
        self.scanResults = {}
    end
    self.scanResults.raidBuffs = self.scanResults.raidBuffs or {}
    self.scanResults.consumables = self.scanResults.consumables or {}
    self.scanResults.inventory = self.scanResults.inventory or {}
    self.scanResults.classBuffs = self.scanResults.classBuffs or {}
    wipe(self.scanResults.raidBuffs)
    wipe(self.scanResults.consumables)
    wipe(self.scanResults.inventory)
    wipe(self.scanResults.classBuffs)
    self.scanInProgress = false
end

function BWV2:GetCurrentContentType()
    local inInstance, instanceType = IsInInstance()
    if not inInstance then
        return "other"
    elseif instanceType == "raid" then
        return "raid"
    else
        return "dungeon"
    end
end

function BWV2:HasClassInGroup(className)
    local inRaid = IsInRaid()
    local groupSize = GetNumGroupMembers()

    if groupSize == 0 then
        local _, playerClass = UnitClass("player")
        return playerClass == className
    end

    for i = 1, groupSize do
        local unit
        if inRaid then
            unit = "raid" .. i
        else
            unit = (i == 1) and "player" or ("party" .. (i - 1))
        end

        if UnitExists(unit) then
            local _, unitClass = UnitClass(unit)
            if unitClass == className then
                return true
            end
        end
    end

    return false
end

function BWV2:GetPlayerSpecID()
    local specIndex = GetSpecialization()
    if specIndex then
        return GetSpecializationInfo(specIndex)
    end
    return nil
end

function BWV2:PlayerHasTalent(spellID)
    if not spellID then return false end
    return ns.IsPlayerSpell(spellID)
end

function BWV2:InitSavedVars()
    if not BrightQoL then BrightQoL = {} end
    if not BrightQoL.buffWatcherV2 then
        BrightQoL.buffWatcherV2 = {
            enabled = true,
            userEntries = {
                raidBuffs = { spellIDs = {} },
                consumables = { spellIDs = {} },
                shamanImbues = { enchantIDs = {} },
                roguePoisons = { enchantIDs = {} },
                shamanShields = { spellIDs = {} },
            },
            categoryEnabled = {
                raidBuffs = true,
                consumables = true,
                shamanImbues = true,
                roguePoisons = true,
                shamanShields = true,
            },
            thresholds = {
                dungeon = 2400,
                raid = 900,
                other = 300,
            },
            talentMods = {
                roguePoisons = {
                    { type = "requireCount", talentID = 381802, count = 4 },
                },
            },
            disabledDefaults = {},
            consumableGroupEnabled = {
                flask = true,
                food = true,
                rune = true,
                weaponBuff = true,
            },
            consumableAutoUse = {
                flask = nil,
                food = nil,
                rune = nil,
                weaponBuff = nil,
            },
            inventoryGroupEnabled = {
                dpsPotion = true,
                healthPotion = true,
                healthstone = true,
                gatewayControl = true,
                manaBun = false,
            },
            classBuffs = {
                WARRIOR     = { enabled = true, groups = {} },
                PALADIN     = { enabled = true, groups = {} },
                HUNTER      = { enabled = true, groups = {} },
                ROGUE       = { enabled = true, groups = {} },
                PRIEST      = { enabled = true, groups = {} },
                DEATHKNIGHT = { enabled = true, groups = {} },
                SHAMAN      = { enabled = true, groups = {} },
                MAGE        = { enabled = true, groups = {} },
                WARLOCK     = { enabled = true, groups = {} },
                MONK        = { enabled = true, groups = {} },
                DRUID       = { enabled = true, groups = {} },
                DEMONHUNTER = { enabled = true, groups = {} },
                EVOKER      = { enabled = true, groups = {} },
            },
            reportCardPosition = nil,
            buffDropPosition = nil,
            reportCardIconSize = 32,
            reportCardUnlock = false,
            reportCardScale = 1.0,
            reportCardAutoCloseDelay = 5,
            scanOnLogin = false,
            lastSection = "classBuffs",
            chatReportEnabled = false,
            buffDropReminder = true,
            buffDropIconSize = 32,
            buffDropScale = 1.0,
            buffDropUnlock = false,
            buffDropGlowR = 0.95,
            buffDropGlowG = 0.95,
            buffDropGlowB = 0.32,
            buffDropGlowUseClassColor = false,
            buffDropAlertInstanceOnly = false,
            buffDropAlertDisableRested = false,
            buffDropTextFontSize = 11,
            raidBuffAlwaysCheck = false,
            classBuffAlwaysCheck = false,
            consumableAlwaysCheck = false,
            inventoryAlwaysCheck = false,
        }
    end

    if not BrightQoL.buffWatcherV2.disabledDefaults then
        BrightQoL.buffWatcherV2.disabledDefaults = {}
    end
    if not BrightQoL.buffWatcherV2.consumableGroupEnabled then
        BrightQoL.buffWatcherV2.consumableGroupEnabled = {
            flask = true,
            food = true,
            rune = true,
            weaponBuff = true,
        }
    end
    if not BrightQoL.buffWatcherV2.inventoryGroupEnabled then
        BrightQoL.buffWatcherV2.inventoryGroupEnabled = {
            healthPotion = true,
            healthstone = true,
            gatewayControl = true,
            manaBun = false,
        }
    end
    if not BrightQoL.buffWatcherV2.consumableAutoUse then
        BrightQoL.buffWatcherV2.consumableAutoUse = {}
    end
    if not BrightQoL.buffWatcherV2.classBuffs then
        BrightQoL.buffWatcherV2.classBuffs = {
            WARRIOR     = { enabled = true, groups = {} },
            PALADIN     = { enabled = true, groups = {} },
            HUNTER      = { enabled = true, groups = {} },
            ROGUE       = { enabled = true, groups = {} },
            PRIEST      = { enabled = true, groups = {} },
            DEATHKNIGHT = { enabled = true, groups = {} },
            SHAMAN      = { enabled = true, groups = {} },
            MAGE        = { enabled = true, groups = {} },
            WARLOCK     = { enabled = true, groups = {} },
            MONK        = { enabled = true, groups = {} },
            DRUID       = { enabled = true, groups = {} },
            DEMONHUNTER = { enabled = true, groups = {} },
            EVOKER      = { enabled = true, groups = {} },
        }
    end
    if BrightQoL.buffWatcherV2.chatReportEnabled == nil then
        BrightQoL.buffWatcherV2.chatReportEnabled = false
    end
    if BrightQoL.buffWatcherV2.reportCardIconSize == nil then
        BrightQoL.buffWatcherV2.reportCardIconSize = 32
    end
    if BrightQoL.buffWatcherV2.reportCardUnlock == nil then
        BrightQoL.buffWatcherV2.reportCardUnlock = false
    end
    if BrightQoL.buffWatcherV2.reportCardScale == nil then
        BrightQoL.buffWatcherV2.reportCardScale = 1.0
    end
    if BrightQoL.buffWatcherV2.reportCardAutoCloseDelay == nil then
        BrightQoL.buffWatcherV2.reportCardAutoCloseDelay = 5
    end
    if BrightQoL.buffWatcherV2.scanOnLogin == nil then
        BrightQoL.buffWatcherV2.scanOnLogin = false
    end
    if BrightQoL.buffWatcherV2.buffDropReminder == nil then
        BrightQoL.buffWatcherV2.buffDropReminder = true
    end
    if BrightQoL.buffWatcherV2.raidBuffAlwaysCheck == nil then
        BrightQoL.buffWatcherV2.raidBuffAlwaysCheck = false
    end
    if BrightQoL.buffWatcherV2.classBuffAlwaysCheck == nil then
        BrightQoL.buffWatcherV2.classBuffAlwaysCheck = false
    end
    if BrightQoL.buffWatcherV2.consumableAlwaysCheck == nil then
        BrightQoL.buffWatcherV2.consumableAlwaysCheck = false
    end
    if BrightQoL.buffWatcherV2.inventoryAlwaysCheck == nil then
        BrightQoL.buffWatcherV2.inventoryAlwaysCheck = false
    end
    if BrightQoL.buffWatcherV2.buffDropIconSize == nil then
        BrightQoL.buffWatcherV2.buffDropIconSize = 32
    end
    if BrightQoL.buffWatcherV2.buffDropScale == nil then
        BrightQoL.buffWatcherV2.buffDropScale = 1.0
    end
    if BrightQoL.buffWatcherV2.buffDropUnlock == nil then
        BrightQoL.buffWatcherV2.buffDropUnlock = false
    end
    if BrightQoL.buffWatcherV2.buffDropGlowType == nil then
        BrightQoL.buffWatcherV2.buffDropGlowType = 4
    end
    if BrightQoL.buffWatcherV2.buffDropGlowR == nil then
        BrightQoL.buffWatcherV2.buffDropGlowR = 0.95
    end
    if BrightQoL.buffWatcherV2.buffDropGlowG == nil then
        BrightQoL.buffWatcherV2.buffDropGlowG = 0.95
    end
    if BrightQoL.buffWatcherV2.buffDropGlowB == nil then
        BrightQoL.buffWatcherV2.buffDropGlowB = 0.32
    end
    if BrightQoL.buffWatcherV2.buffDropGlowUseClassColor == nil then
        BrightQoL.buffWatcherV2.buffDropGlowUseClassColor = false
    end
    if BrightQoL.buffWatcherV2.buffDropGlowPixelLines == nil then
        BrightQoL.buffWatcherV2.buffDropGlowPixelLines = 8
    end
    if BrightQoL.buffWatcherV2.buffDropGlowPixelFrequency == nil then
        BrightQoL.buffWatcherV2.buffDropGlowPixelFrequency = 0.25
    end
    if BrightQoL.buffWatcherV2.buffDropGlowPixelLength == nil then
        BrightQoL.buffWatcherV2.buffDropGlowPixelLength = 4
    end
    if BrightQoL.buffWatcherV2.buffDropGlowAutocastParticles == nil then
        BrightQoL.buffWatcherV2.buffDropGlowAutocastParticles = 4
    end
    if BrightQoL.buffWatcherV2.buffDropGlowAutocastFrequency == nil then
        BrightQoL.buffWatcherV2.buffDropGlowAutocastFrequency = 0.125
    end
    if BrightQoL.buffWatcherV2.buffDropGlowAutocastScale == nil then
        BrightQoL.buffWatcherV2.buffDropGlowAutocastScale = 1.0
    end
    if BrightQoL.buffWatcherV2.buffDropGlowBorderFrequency == nil then
        BrightQoL.buffWatcherV2.buffDropGlowBorderFrequency = 0.125
    end
    if BrightQoL.buffWatcherV2.buffDropGlowProcDuration == nil then
        BrightQoL.buffWatcherV2.buffDropGlowProcDuration = 1
    end
    if BrightQoL.buffWatcherV2.buffDropGlowProcStartAnim == nil then
        BrightQoL.buffWatcherV2.buffDropGlowProcStartAnim = false
    end
    if BrightQoL.buffWatcherV2.buffDropAlertInstanceOnly == nil then
        BrightQoL.buffWatcherV2.buffDropAlertInstanceOnly = false
    end
    if BrightQoL.buffWatcherV2.buffDropAlertDisableRested == nil then
        BrightQoL.buffWatcherV2.buffDropAlertDisableRested = false
    end
    if BrightQoL.buffWatcherV2.buffDropTextFontSize == nil then
        BrightQoL.buffWatcherV2.buffDropTextFontSize = 11
    end

    if not BrightQoL.buffWatcherV2._classBuffDefaultsVersion then
        local Categories = ns.BWV2Categories
        local defaults = Categories and Categories.DEFAULT_CLASS_BUFFS
        if defaults and BrightQoL.buffWatcherV2.classBuffs then
            for className, classData in pairs(BrightQoL.buffWatcherV2.classBuffs) do
                if classData.groups and #classData.groups == 0 and defaults[className] then
                    for _, group in ipairs(defaults[className]) do
                        local copy = {}
                        for k, v in pairs(group) do
                            if type(v) == "table" then
                                local t = {}
                                for k2, v2 in pairs(v) do
                                    t[k2] = v2
                                end
                                copy[k] = t
                            else
                                copy[k] = v
                            end
                        end
                        classData.groups[#classData.groups + 1] = copy
                    end
                end
            end
        end
        BrightQoL.buffWatcherV2._classBuffDefaultsVersion = 1
    end

    if (BrightQoL.buffWatcherV2._classBuffDefaultsVersion or 0) < 4 then
        local Categories = ns.BWV2Categories
        local defaults = Categories and Categories.DEFAULT_CLASS_BUFFS
        if defaults and BrightQoL.buffWatcherV2.classBuffs and BrightQoL.buffWatcherV2.classBuffs["SHAMAN"] then
            local shamanData = BrightQoL.buffWatcherV2.classBuffs["SHAMAN"]
            local newGroups = {}
            for _, group in ipairs(defaults["SHAMAN"]) do
                local copy = {}
                for k, v in pairs(group) do
                    if type(v) == "table" then
                        local t = {}
                        for k2, v2 in pairs(v) do t[k2] = v2 end
                        copy[k] = t
                    else
                        copy[k] = v
                    end
                end
                newGroups[#newGroups + 1] = copy
            end
            shamanData.groups = newGroups
        end
        BrightQoL.buffWatcherV2._classBuffDefaultsVersion = 4
    end

    if (BrightQoL.buffWatcherV2._classBuffDefaultsVersion or 0) < 5 then
        local Categories = ns.BWV2Categories
        local defaults = Categories and Categories.DEFAULT_CLASS_BUFFS
        if defaults and BrightQoL.buffWatcherV2.classBuffs and BrightQoL.buffWatcherV2.classBuffs["ROGUE"] then
            local rogueData = BrightQoL.buffWatcherV2.classBuffs["ROGUE"]
            local newGroups = {}
            for _, group in ipairs(defaults["ROGUE"]) do
                local copy = {}
                for k, v in pairs(group) do
                    if type(v) == "table" then
                        local t = {}
                        for k2, v2 in pairs(v) do t[k2] = v2 end
                        copy[k] = t
                    else
                        copy[k] = v
                    end
                end
                newGroups[#newGroups + 1] = copy
            end
            rogueData.groups = newGroups
        end
        BrightQoL.buffWatcherV2._classBuffDefaultsVersion = 5
    end

    -- v6: Add exclusiveGroup to shaman shield entries
    if (BrightQoL.buffWatcherV2._classBuffDefaultsVersion or 0) < 6 then
        if BrightQoL.buffWatcherV2.classBuffs and BrightQoL.buffWatcherV2.classBuffs["SHAMAN"] then
            local shamData = BrightQoL.buffWatcherV2.classBuffs["SHAMAN"]
            local exclusiveKeys = { shamanShield = true, earth_shield = true, water_shield = true }
            for _, group in ipairs(shamData.groups or {}) do
                if exclusiveKeys[group.key] then
                    group.exclusiveGroup = "shamanShields"
                end
            end
        end
        BrightQoL.buffWatcherV2._classBuffDefaultsVersion = 6
    end

    -- v7: Fix Earth Shield checkType "self" -> "targeted"
    if (BrightQoL.buffWatcherV2._classBuffDefaultsVersion or 0) < 7 then
        if BrightQoL.buffWatcherV2.classBuffs and BrightQoL.buffWatcherV2.classBuffs["SHAMAN"] then
            local shamData = BrightQoL.buffWatcherV2.classBuffs["SHAMAN"]
            for _, group in ipairs(shamData.groups or {}) do
                if group.key == "earth_shield" and group.checkType == "self" then
                    group.checkType = "targeted"
                end
            end
        end
        BrightQoL.buffWatcherV2._classBuffDefaultsVersion = 7
    end

    -- v8: Fix Earth Shield spell ID to 974 (targetted cast) + add Tidecaller's Guard
    if (BrightQoL.buffWatcherV2._classBuffDefaultsVersion or 0) < 8 then
        if BrightQoL.buffWatcherV2.classBuffs and BrightQoL.buffWatcherV2.classBuffs["SHAMAN"] then
            local shamData = BrightQoL.buffWatcherV2.classBuffs["SHAMAN"]
            for _, group in ipairs(shamData.groups or {}) do
                if group.key == "earth_shield" then
                    group.spellIDs = {974}
                    if group.talentCondition then
                        group.talentCondition.talentID = 974
                    end
                end
            end

            local hasTidecaller = false
            for _, group in ipairs(shamData.groups or {}) do
                if group.key == "tidecallersGuard" then
                    hasTidecaller = true
                    break
                end
            end
            if not hasTidecaller then
                shamData.groups[#shamData.groups + 1] = {
                    key = "tidecallersGuard",
                    name = "Tidecaller's Guard",
                    checkType = "weaponEnchant",
                    enchantIDs = {7528},
                    specFilter = {264},
                    minRequired = 1,
                    thresholds = { dungeon = 0, raid = 0, other = 0 },
                    talentCondition = { talentID = 457481, mode = "activate" },
                }
            end
        end
        BrightQoL.buffWatcherV2._classBuffDefaultsVersion = 8
    end

    -- v9: Rebuild SHAMAN groups from defaults (Elemental Orbit support)
    if (BrightQoL.buffWatcherV2._classBuffDefaultsVersion or 0) < 9 then
        local Categories = ns.BWV2Categories
        local defaults = Categories and Categories.DEFAULT_CLASS_BUFFS
        if defaults and BrightQoL.buffWatcherV2.classBuffs and BrightQoL.buffWatcherV2.classBuffs["SHAMAN"] then
            local shamData = BrightQoL.buffWatcherV2.classBuffs["SHAMAN"]
            local newGroups = {}
            for _, group in ipairs(defaults["SHAMAN"]) do
                local copy = {}
                for k, v in pairs(group) do
                    if type(v) == "table" then
                        local t = {}
                        for k2, v2 in pairs(v) do t[k2] = v2 end
                        copy[k] = t
                    else
                        copy[k] = v
                    end
                end
                newGroups[#newGroups + 1] = copy
            end
            shamData.groups = newGroups
        end
        BrightQoL.buffWatcherV2._classBuffDefaultsVersion = 9
    end
end

function BWV2:GetDB()
    self:InitSavedVars()
    return BrightQoL.buffWatcherV2
end

function BWV2:GetBuffDropFont()
    local db = self:GetDB()
    if db.buffDropTextFont then
        return ns.Media.ResolveFont(db.buffDropTextFont)
    end
    return ns.DefaultFontPath()
end

function BWV2:IsEnabled()
    local db = self:GetDB()
    return db.enabled
end

function BWV2:GetThreshold()
    local db = self:GetDB()
    local contentType = self:GetCurrentContentType()
    return db.thresholds[contentType] or 300
end

function BWV2:BuildBuffSnapshot()
    wipe(self.buffSnapshot)
    wipe(self.buffDropReminded)

    local results = self.scanResults
    local Categories = ns.BWV2Categories
    if not results or not Categories then return end

    for _, entry in ipairs(results.raidBuffs or {}) do
        if entry.pass then
            for _, buff in ipairs(Categories.RAID) do
                if buff.key == entry.key then
                    local ids = type(buff.spellID) == "table" and buff.spellID or {buff.spellID}
                    local playerKnows = false
                    for _, spellID in ipairs(ids) do
                        if ns.IsPlayerSpell(spellID) then
                            playerKnows = true
                            break
                        end
                    end
                    if playerKnows then
                        self.buffSnapshot[entry.key] = {
                            name = entry.name,
                            spellIDs = ids,
                            icon = entry.icon,
                            category = "raidBuff",
                        }
                    end
                    break
                end
            end
        end
    end

    for _, entry in ipairs(results.consumables or {}) do
        if entry.pass and not entry.unconfigured then
            local ids = {}
            local iconCheck = nil
            for _, grp in ipairs(Categories.CONSUMABLE_GROUPS) do
                if grp.key == entry.key then
                    if grp.checkType == "icon" and grp.buffIconID then
                        iconCheck = grp.buffIconID
                    elseif grp.checkType == "weaponEnchant" then
                        self.buffSnapshot[entry.key] = {
                            name = entry.name,
                            icon = entry.icon,
                            category = "consumable",
                            checkType = "weaponEnchant",
                        }
                        break
                    elseif grp.spellIDs then
                        for _, id in ipairs(grp.spellIDs) do
                            ids[#ids + 1] = id
                        end
                    end
                    break
                end
            end
            if #ids > 0 or iconCheck then
                self.buffSnapshot[entry.key] = {
                    name = entry.name,
                    spellIDs = ids,
                    icon = entry.icon,
                    category = "consumable",
                    iconCheck = iconCheck,
                }
            end
        end
    end

    for _, entry in ipairs(results.classBuffs or {}) do
        if entry.pass then
            local _, playerClass = UnitClass("player")
            local db = self:GetDB()
            local classData = db.classBuffs and db.classBuffs[playerClass]
            if classData then
                for _, group in ipairs(classData.groups or {}) do
                    if group.key == entry.key then
                        if entry.checkType == "self" and group.spellIDs then
                            self.buffSnapshot[entry.key] = {
                                name = entry.name,
                                spellIDs = group.spellIDs,
                                icon = entry.icon,
                                category = "classBuff",
                                thresholds = group.thresholds,
                            }
                        elseif entry.checkType == "weaponEnchant" and group.enchantIDs then
                            self.buffSnapshot[entry.key] = {
                                name = entry.name,
                                icon = entry.icon,
                                category = "classBuff",
                                checkType = "weaponEnchant",
                                enchantIDs = group.enchantIDs,
                                minRequired = group.minRequired or 1,
                            }
                        end
                        break
                    end
                end
            end
        end
    end
end

function BWV2:CheckBuffDrops()
    if not self.buffSnapshot or not next(self.buffSnapshot) then
        return nil
    end
    if not ns.DisplayUtils.CanReadAuras() then return nil end
    local db = self:GetDB()
    local suppressForInstance = db.buffDropAlertInstanceOnly and not ns.ZoneUtil.IsInDungeonOrRaid()
    local suppressForRested = db.buffDropAlertDisableRested and IsResting()

    local dropped = {}

    for key, data in pairs(self.buffSnapshot) do
        local isPersonalBuff = data.category == "raidBuff" or data.category == "classBuff"
        if not (suppressForRested or (suppressForInstance and not isPersonalBuff)) and not self.buffDropReminded[key] then
            local stillPresent = false
            local foundExpiry = nil

            if data.spellIDs and #data.spellIDs > 0 then
                local contentType = self:GetCurrentContentType()
                local threshold
                if data.category == "classBuff" and data.thresholds then
                    threshold = data.thresholds[contentType] or 0
                else
                    threshold = self:GetThreshold()
                end
                for _, spellID in ipairs(data.spellIDs) do
                    local aura = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
                    if aura then
                        local expTime = aura.expirationTime
                        if IsSecret(expTime) then
                            stillPresent = true
                            break
                        end
                        local remaining = (expTime or 0) - GetTime()
                        if expTime == 0 or remaining > threshold then
                            stillPresent = true
                            break
                        elseif expTime ~= 0 then
                            foundExpiry = expTime
                        end
                    end
                end
            end

            if not stillPresent and data.iconCheck and not InCombatLockdown() then
                local threshold = self:GetThreshold()
                local idx = 1
                local auraData = C_UnitAuras.GetAuraDataByIndex("player", idx, "HELPFUL")
                while auraData do
                    local icon = auraData.icon
                    if not IsSecret(icon) and icon == data.iconCheck then
                        local expTime = auraData.expirationTime
                        if IsSecret(expTime) then
                            stillPresent = true
                        else
                            local remaining = (expTime or 0) - GetTime()
                            if expTime == 0 or remaining > threshold then
                                stillPresent = true
                            elseif expTime ~= 0 then
                                foundExpiry = expTime
                            end
                        end
                        break
                    end
                    idx = idx + 1
                    auraData = C_UnitAuras.GetAuraDataByIndex("player", idx, "HELPFUL")
                end
            end

            if not stillPresent and data.checkType == "weaponEnchant" then
                local wOk, hasMain, _, _, mainID, hasOff, _, _, offID = pcall(GetWeaponEnchantInfo)
                if wOk then
                    if data.enchantIDs and #data.enchantIDs > 0 then
                        local count = 0
                        for _, eid in ipairs(data.enchantIDs) do
                            if (hasMain and mainID == eid) or (hasOff and offID == eid) then
                                count = count + 1
                            end
                        end
                        local needed = (data.minRequired == 0) and #data.enchantIDs or (data.minRequired or 1)
                        stillPresent = count >= needed
                    else
                        stillPresent = hasMain and true or false
                    end
                end
            end

            if not stillPresent then
                local entry = {}
                for k, v in pairs(data) do entry[k] = v end
                entry.key = key
                if foundExpiry then
                    entry.expiryTime = foundExpiry
                end
                dropped[#dropped + 1] = entry
                self.buffDropReminded[key] = true
            end
        end
    end

    return (#dropped > 0) and dropped or nil
end

function BWV2:AddToBuffSnapshot(item, categoryKey)
    if not item or not item.key then return end
    local Categories = ns.BWV2Categories
    if not Categories then return end

    if categoryKey == "raidBuffs" then
        for _, buff in ipairs(Categories.RAID) do
            if buff.key == item.key then
                local ids = type(buff.spellID) == "table" and buff.spellID or {buff.spellID}
                local playerKnows = false
                for _, spellID in ipairs(ids) do
                    if ns.IsPlayerSpell(spellID) then
                        playerKnows = true
                        break
                    end
                end
                if playerKnows then
                    self.buffSnapshot[item.key] = {
                        name = item.name,
                        spellIDs = ids,
                        icon = item.icon,
                        category = "raidBuff",
                    }
                end
                return
            end
        end
    elseif categoryKey == "consumables" then
        for _, grp in ipairs(Categories.CONSUMABLE_GROUPS) do
            if grp.key == item.key then
                local ids = {}
                local iconCheck = nil
                if grp.checkType == "icon" and grp.buffIconID then
                    iconCheck = grp.buffIconID
                elseif grp.checkType == "weaponEnchant" then
                    self.buffSnapshot[item.key] = {
                        name = item.name,
                        icon = item.icon,
                        category = "consumable",
                        checkType = "weaponEnchant",
                    }
                    return
                elseif grp.spellIDs then
                    for _, id in ipairs(grp.spellIDs) do
                        ids[#ids + 1] = id
                    end
                end
                if #ids > 0 or iconCheck then
                    self.buffSnapshot[item.key] = {
                        name = item.name,
                        spellIDs = ids,
                        icon = item.icon,
                        category = "consumable",
                        iconCheck = iconCheck,
                    }
                end
                return
            end
        end
    elseif categoryKey == "classBuffs" then
        local _, playerClass = UnitClass("player")
        local db = self:GetDB()
        local classData = db.classBuffs and db.classBuffs[playerClass]
        if classData then
            for _, group in ipairs(classData.groups or {}) do
                if group.key == item.key then
                    if item.checkType == "self" and group.spellIDs then
                        self.buffSnapshot[item.key] = {
                            name = item.name,
                            spellIDs = group.spellIDs,
                            icon = item.icon,
                            category = "classBuff",
                            thresholds = group.thresholds,
                        }
                    elseif item.checkType == "weaponEnchant" and group.enchantIDs then
                        self.buffSnapshot[item.key] = {
                            name = item.name,
                            icon = item.icon,
                            category = "classBuff",
                            checkType = "weaponEnchant",
                            enchantIDs = group.enchantIDs,
                            minRequired = group.minRequired or 1,
                        }
                    end
                    return
                end
            end
        end
    end
end

function BWV2:CheckAlwaysOnRaidBuffs()
    local db = self:GetDB()
    if not db or not db.raidBuffAlwaysCheck then return nil end
    if ns.ZoneUtil.IsInPvP() then return nil end
    if db.buffDropAlertDisableRested and IsResting() then return nil end
    if InCombatLockdown() then return nil end
    if not ns.DisplayUtils.CanReadGroupAuras() then return nil end

    local Categories = ns.BWV2Categories
    if not Categories then return nil end

    local units = {}
    local inRaid = IsInRaid()
    local groupSize = GetNumGroupMembers()
    if groupSize == 0 then
        units[1] = "player"
    else
        for i = 1, groupSize do
            local unit
            if inRaid then
                unit = "raid" .. i
            else
                unit = (i == 1) and "player" or ("party" .. (i - 1))
            end
            if UnitExists(unit) and UnitIsConnected(unit) and UnitIsVisible(unit) then
                units[#units + 1] = unit
            end
        end
    end

    if #units == 0 then return nil end

    local threshold = self:GetThreshold()
    local missing = {}

    for _, buff in ipairs(Categories.RAID) do
        local primaryID = type(buff.spellID) == "table" and buff.spellID[1] or buff.spellID
        if primaryID and not Categories:IsDefaultDisabled("raidBuffs", primaryID)
           and Categories:IsCategoryEnabled(buff.key) then
            local playerKnows = false
            local spellIDs = type(buff.spellID) == "table" and buff.spellID or {buff.spellID}
            for _, spellID in ipairs(spellIDs) do
                if ns.IsPlayerSpell(spellID) then
                    playerKnows = true
                    break
                end
            end

            if playerKnows then
                local covered = 0
                local total = #units
                for _, unit in ipairs(units) do
                    local idx = 1
                    local auraData = C_UnitAuras.GetAuraDataByIndex(unit, idx, "HELPFUL")
                    local hasBuff = false
                    while auraData do
                        if IsSecret(auraData.spellId) then return nil end
                        for _, spellID in ipairs(spellIDs) do
                            if auraData.spellId == spellID then
                                local expTime = auraData.expirationTime
                                if IsSecret(expTime) then
                                    hasBuff = true
                                elseif expTime == 0 or (expTime - GetTime()) > threshold then
                                    hasBuff = true
                                end
                                break
                            end
                        end
                        if hasBuff then break end
                        idx = idx + 1
                        auraData = C_UnitAuras.GetAuraDataByIndex(unit, idx, "HELPFUL")
                    end
                    if hasBuff then covered = covered + 1 end
                end

                if covered < total then
                    local icon = C_Spell.GetSpellTexture(primaryID)
                    missing[#missing + 1] = {
                        key = "raidAlways_" .. buff.key,
                        name = buff.name,
                        spellIDs = spellIDs,
                        icon = icon,
                        category = "raidBuff",
                        covered = covered,
                        total = total,
                        isGroupCoverage = true,
                    }
                end
            end
        end
    end

    return (#missing > 0) and missing or nil
end

function BWV2:CheckAlwaysOnClassBuffs()
    local db = self:GetDB()
    if not db or not db.classBuffAlwaysCheck then return nil end
    if ns.ZoneUtil.IsInPvP() then return nil end
    if db.buffDropAlertDisableRested and IsResting() then return nil end
    if InCombatLockdown() then return nil end
    if not ns.DisplayUtils.CanReadAuras() then return nil end

    local _, playerClass = UnitClass("player")
    local classData = db.classBuffs and db.classBuffs[playerClass]
    if not classData or not classData.enabled then return nil end

    local playerSpecID = self:GetPlayerSpecID()
    local missing = {}

    for _, group in ipairs(classData.groups or {}) do
        local shouldCheck = true

        if group.specFilter and #group.specFilter > 0 then
            local specMatch = false
            for _, specID in ipairs(group.specFilter) do
                if specID == playerSpecID then
                    specMatch = true
                    break
                end
            end
            if not specMatch then shouldCheck = false end
        end

        if shouldCheck and group.talentCondition then
            local hasTalent = self:PlayerHasTalent(group.talentCondition.talentID)
            if group.talentCondition.mode == "activate" and not hasTalent then
                shouldCheck = false
            elseif group.talentCondition.mode == "skip" and hasTalent then
                shouldCheck = false
            end
        end

        if shouldCheck then
            local hasBuff = false
            local icon = nil

            if group.checkType == "self" then
                local spellIDs = group.spellIDs or {}
                local contentType = self:GetCurrentContentType()
                local threshold = (group.thresholds and group.thresholds[contentType]) or self:GetThreshold()
                local needed = (group.minRequired == 0) and #spellIDs or (group.minRequired or 1)
                local count = 0
                for _, spellID in ipairs(spellIDs) do
                    local aura = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
                    if aura then
                        local expTime = aura.expirationTime
                        if IsSecret(expTime) then
                            hasBuff = true
                            break
                        end
                        local remaining = (expTime or 0) - GetTime()
                        if expTime == 0 or remaining > threshold then
                            count = count + 1
                        end
                    end
                end
                hasBuff = count >= needed
                -- Out of combat: cache state and auraInstanceID for combat tracking.
                -- In combat: aura APIs return tainted/nil, so trust the cache
                -- (cache is invalidated by OnClassBuffAuraEvent when removals are detected)
                if not InCombatLockdown() then
                    self.classBuffSelfCache[group.key] = hasBuff
                    if hasBuff then
                        for _, spellID in ipairs(spellIDs) do
                            local auraData = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
                            if auraData and auraData.auraInstanceID then
                                self.classBuffInstanceIDs[auraData.auraInstanceID] = group.key
                            end
                        end
                    end
                elseif not hasBuff and self.classBuffSelfCache[group.key] then
                    hasBuff = true
                end
                if spellIDs[1] then
                    icon = C_Spell.GetSpellTexture(spellIDs[1])
                end
            elseif group.checkType == "targeted" then
                local spellIDs = group.spellIDs or {}
                local threshold = self:GetThreshold()
                if GetNumGroupMembers() == 0 then
                    hasBuff = true
                elseif InCombatLockdown() then
                    hasBuff = true
                elseif not ns.DisplayUtils.CanReadGroupAuras() then
                    hasBuff = true
                elseif #spellIDs > 0 then
                    local inRaid = IsInRaid()
                    local groupSize = GetNumGroupMembers()
                    if groupSize == 0 then groupSize = 1 end
                    for i = 1, groupSize do
                        local unit
                        if inRaid then
                            unit = "raid" .. i
                        else
                            unit = (i == 1) and "player" or ("party" .. (i - 1))
                        end
                        if UnitExists(unit) then
                            local idx = 1
                            local auraData = C_UnitAuras.GetAuraDataByIndex(unit, idx, "HELPFUL")
                            while auraData do
                                if IsSecret(auraData.spellId) then
                                    hasBuff = true
                                    break
                                end
                                for _, spellID in ipairs(spellIDs) do
                                    if auraData.spellId == spellID
                                       and auraData.sourceUnit
                                       and UnitIsUnit(auraData.sourceUnit, "player") then
                                        local expTime = auraData.expirationTime
                                        if IsSecret(expTime) then
                                            hasBuff = true
                                        elseif expTime == 0 or (expTime - GetTime()) > threshold then
                                            hasBuff = true
                                        end
                                        break
                                    end
                                end
                                if hasBuff then break end
                                idx = idx + 1
                                auraData = C_UnitAuras.GetAuraDataByIndex(unit, idx, "HELPFUL")
                            end
                        end
                        if hasBuff then break end
                    end
                end
                if spellIDs[1] then
                    icon = C_Spell.GetSpellTexture(spellIDs[1])
                end
            elseif group.checkType == "weaponEnchant" then
                local enchantIDs = group.enchantIDs or {}
                if #enchantIDs > 0 then
                    local wOk, hasMain, _, _, mainID, hasOff, _, _, offID = pcall(GetWeaponEnchantInfo)
                    if wOk then
                        local count = 0
                        for _, eid in ipairs(enchantIDs) do
                            if (hasMain and mainID == eid) or (hasOff and offID == eid) then
                                count = count + 1
                            end
                        end
                        local needed = (group.minRequired == 0) and #enchantIDs or (group.minRequired or 1)
                        hasBuff = count >= needed
                    end
                end
                local scanner = ns.BWV2Scanner
                icon = (scanner and #(group.enchantIDs or {}) > 0 and scanner:GetEnchantIcon(group.enchantIDs[1])) or 463543
            end

            if not hasBuff then
                missing[#missing + 1] = {
                    key = "classAlways_" .. group.key,
                    name = group.name,
                    icon = icon or 134400,
                    category = "classBuff",
                    checkType = group.checkType,
                    spellIDs = group.spellIDs,
                    enchantIDs = group.enchantIDs,
                    minRequired = group.minRequired,
                }
            end
        end
    end

    return (#missing > 0) and missing or nil
end

function BWV2:CheckAlwaysOnConsumables()
    local db = self:GetDB()
    if not db or not db.consumableAlwaysCheck then return nil end
    if InCombatLockdown() then return nil end
    if not ns.DisplayUtils.CanReadAuras() then return nil end
    if self:ShouldSuppressAlertsForZone() then return nil end
    if self.isDead then return nil end

    local Categories = ns.BWV2Categories
    if not Categories then return nil end

    local missing = {}
    local threshold = self:GetThreshold()

    for _, buff in ipairs(Categories.CONSUMABLE_GROUPS) do
        if not Categories:IsConsumableGroupEnabled(buff.key) then
        elseif not Categories:IsCategoryEnabled(buff.key) then
        else
            local skip = false
            if buff.excludeIfSpellKnown then
                for _, spellID in ipairs(buff.excludeIfSpellKnown) do
                    if ns.IsPlayerSpell(spellID) then
                        skip = true
                        break
                    end
                end
            end

            if not skip then
                local spellIDs = {}
                for _, id in ipairs(buff.spellIDs or {}) do
                    spellIDs[#spellIDs + 1] = id
                end
                local userEntries = db.userEntries and db.userEntries["consumable_" .. buff.key]
                if userEntries and userEntries.spellIDs then
                    for _, id in ipairs(userEntries.spellIDs) do
                        spellIDs[#spellIDs + 1] = id
                    end
                end

                local hasBuff = false
                local icon = nil
                local foundExpiry = nil

                if buff.checkType == "icon" and buff.buffIconID then
                    local idx = 1
                    local auraData = C_UnitAuras.GetAuraDataByIndex("player", idx, "HELPFUL")
                    while auraData do
                        if not IsSecret(auraData.icon) and auraData.icon == buff.buffIconID then
                            local expTime = auraData.expirationTime
                            if IsSecret(expTime) then
                                hasBuff = true
                                icon = auraData.icon
                                break
                            end
                            local remaining = (expTime or 0) - GetTime()
                            if expTime == 0 or remaining > threshold then
                                hasBuff = true
                                icon = auraData.icon
                                break
                            elseif expTime ~= 0 then
                                foundExpiry = expTime
                            end
                        end
                        idx = idx + 1
                        auraData = C_UnitAuras.GetAuraDataByIndex("player", idx, "HELPFUL")
                    end
                elseif buff.checkType == "weaponEnchant" then
                    local success = Categories:CheckWeaponBuffStatus()
                    hasBuff = success
                    icon = buff.fallbackIcon or 463543
                elseif buff.itemIDs and #buff.itemIDs > 0 then
                    hasBuff = Categories:HasInventoryItem(buff.itemIDs)
                elseif #spellIDs > 0 then
                    for _, spellID in ipairs(spellIDs) do
                        local aura = C_UnitAuras.GetPlayerAuraBySpellID(spellID)
                        if aura then
                            local expTime = aura.expirationTime
                            if IsSecret(expTime) then
                                hasBuff = true
                                icon = aura.icon or C_Spell.GetSpellTexture(spellID) or buff.fallbackIcon
                                break
                            end
                            local remaining = (expTime or 0) - GetTime()
                            if expTime == 0 or remaining > threshold then
                                hasBuff = true
                                icon = aura.icon or C_Spell.GetSpellTexture(spellID) or buff.fallbackIcon
                                break
                            elseif expTime ~= 0 then
                                foundExpiry = expTime
                                icon = aura.icon or C_Spell.GetSpellTexture(spellID) or buff.fallbackIcon
                            end
                        end
                    end
                    if not icon and spellIDs[1] then
                        icon = C_Spell.GetSpellTexture(spellIDs[1]) or buff.fallbackIcon
                    end
                end

                if not hasBuff then
                    missing[#missing + 1] = {
                        key = "consumableAlways_" .. buff.key,
                        name = buff.name,
                        icon = icon or buff.fallbackIcon,
                        category = "consumable",
                        expiryTime = foundExpiry,
                    }
                end
            end
        end
    end

    return (#missing > 0) and missing or nil
end

function BWV2:CheckAlwaysOnInventory()
    local db = self:GetDB()
    if not db or not db.inventoryAlwaysCheck then return nil end
    if InCombatLockdown() then return nil end
    if self:ShouldSuppressAlertsForZone() then return nil end

    local Categories = ns.BWV2Categories
    if not Categories then return nil end

    local missing = {}

    for _, group in ipairs(Categories.INVENTORY_GROUPS) do
        if not Categories:IsInventoryGroupEnabled(group.key) then
        elseif group.requireClass and not BWV2:HasClassInGroup(group.requireClass) then
        else
            local itemIDs = {}
            local disabledDefaults = db.disabledDefaults and db.disabledDefaults["inventory_" .. group.key] or {}
            for _, itemID in ipairs(group.itemIDs or {}) do
                if not disabledDefaults[itemID] then
                    itemIDs[#itemIDs + 1] = itemID
                end
            end
            local userEntries = db.userEntries and db.userEntries["inventory_" .. group.key]
            if userEntries and userEntries.itemIDs then
                for _, itemID in ipairs(userEntries.itemIDs) do
                    itemIDs[#itemIDs + 1] = itemID
                end
            end
            if #itemIDs > 0 and Categories:GetInventoryItemCount(itemIDs) == 0 then
                local _, _, _, _, _, _, _, _, _, icon = C_Item.GetItemInfo(itemIDs[1])
                missing[#missing + 1] = {
                    key = "inventoryAlways_" .. group.key,
                    name = group.name,
                    icon = icon or group.fallbackIcon or 134400,
                    category = "inventory",
                    checkType = "inventory",
                }
            end
        end
    end

    return (#missing > 0) and missing or nil
end

function BWV2:SetDeadState(dead)
    local changed = self.isDead ~= dead
    self.isDead = dead
    if changed then
        self:SetDirty()
    end
end

function BWV2:OnClassBuffAuraEvent(updateInfo)
    if not InCombatLockdown() or not updateInfo then return end

    -- Track removals: if a tracked auraInstanceID was removed, invalidate cache
    if updateInfo.removedAuraInstanceIDs then
        for _, instanceID in ipairs(updateInfo.removedAuraInstanceIDs) do
            local ok, groupKey = pcall(function() return self.classBuffInstanceIDs[instanceID] end)
            if ok and groupKey then
                self.classBuffSelfCache[groupKey] = false
                self.classBuffInstanceIDs[instanceID] = nil
            end
        end
    end

    -- Note: addedAuras fields are fully tainted in combat, so we cannot detect
    -- re-application here. The cache will be corrected on PLAYER_REGEN_ENABLED
    -- when aura APIs become readable again.
end

function BWV2:ClearBuffSnapshot()
    wipe(self.buffSnapshot)
    wipe(self.buffDropReminded)
end

function BWV2:ShouldSuppressAlertsForZone()
    local db = self:GetDB()
    if ns.ZoneUtil.IsInPvP() then
        return true
    end
    if db.buffDropAlertInstanceOnly and not ns.ZoneUtil.IsInDungeonOrRaid() then
        return true
    end
    if db.buffDropAlertDisableRested and IsResting() then
        return true
    end
    return false
end
