-- DynamicTooltips: Math Calculation Module
-- Handles spell coefficient calculations and stat scaling

local addonName, DT = ...

DT.Math = {}

-- ============================================================================
-- SPELL COEFFICIENTS DATABASE
-- Loaded from CSV data - Format: [spellID] = { sp = spellPowerCoeff, wd = weaponDamageCoeff, type = "type", base = {min, max}, ticks = numTicks, duration = seconds }
-- ============================================================================

DT.SpellData = {
    -- ===================
    -- PRIEST SPELLS
    -- ===================
    -- Holy/Discipline Healing
    [2061] = { sp = 0.429, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Flash Heal" },
    [2060] = { sp = 0.857, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Greater Heal" },
    [2050] = { sp = 0.476, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Lesser Heal" },
    [596] = { sp = 0.286, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Prayer of Healing" },
    [33076] = { sp = 0.429, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Prayer of Mending" },
    [34861] = { sp = 0.286, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Circle of Healing" },
    [32546] = { sp = 0.429, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Binding Heal" },
    [139] = { sp = 1.0, wd = 0, type = "HoT", school = "Holy", isHeal = true, ticks = 5, duration = 15, name = "Renew" },
    [17] = { sp = 0.3, wd = 0, type = "Shield", school = "Holy", isHeal = true, name = "Power Word: Shield" },
    
    -- Holy Damage
    [585] = { sp = 0.714, wd = 0, type = "Direct", school = "Holy", name = "Smite" },
    [14914] = { sp = 0.571, wd = 0, type = "Direct", school = "Holy", name = "Holy Fire" },
    [15237] = { sp = 0.143, wd = 0, type = "Direct", school = "Holy", name = "Holy Nova" },
    
    -- Shadow
    [589] = { sp = 1.1, wd = 0, type = "DoT", school = "Shadow", ticks = 6, duration = 18, name = "Shadow Word: Pain" },
    [8092] = { sp = 0.429, wd = 0, type = "Direct", school = "Shadow", name = "Mind Blast" },
    [15407] = { sp = 0.571, wd = 0, type = "Channel", school = "Shadow", ticks = 3, duration = 3, name = "Mind Flay" },
    [32379] = { sp = 0.429, wd = 0, type = "Direct", school = "Shadow", name = "Shadow Word: Death" },
    [34914] = { sp = 1.0, wd = 0, type = "DoT", school = "Shadow", ticks = 5, duration = 15, name = "Vampiric Touch" },
    [2944] = { sp = 0.8, wd = 0, type = "DoT", school = "Shadow", ticks = 8, duration = 24, name = "Devouring Plague" },
    
    -- ===================
    -- WARLOCK SPELLS
    -- ===================
    [686] = { sp = 0.857, wd = 0, type = "Direct", school = "Shadow", name = "Shadow Bolt" },
    [172] = { sp = 0.936, wd = 0, type = "DoT", school = "Shadow", ticks = 6, duration = 18, name = "Corruption" },
    [348] = { sp = 0.2, wd = 0, type = "Direct", school = "Fire", dotSp = 0.65, dotTicks = 5, dotDuration = 15, name = "Immolate" },
    [980] = { sp = 1.2, wd = 0, type = "DoT", school = "Shadow", ticks = 12, duration = 24, name = "Curse of Agony" },
    [603] = { sp = 2.0, wd = 0, type = "DoT", school = "Shadow", ticks = 1, duration = 60, name = "Curse of Doom" },
    [29722] = { sp = 0.714, wd = 0, type = "Direct", school = "Fire", name = "Incinerate" },
    [17962] = { sp = 0.714, wd = 0, type = "Direct", school = "Fire", name = "Conflagrate" },
    [5676] = { sp = 0.429, wd = 0, type = "Direct", school = "Fire", name = "Searing Pain" },
    [6353] = { sp = 1.15, wd = 0, type = "Direct", school = "Fire", name = "Soul Fire" },
    [30108] = { sp = 1.0, wd = 0, type = "DoT", school = "Shadow", ticks = 5, duration = 15, name = "Unstable Affliction" },
    [27243] = { sp = 0.214, wd = 0, type = "DoT", school = "Shadow", aoesp = 0.286, name = "Seed of Corruption" },
    [689] = { sp = 0.714, wd = 0, type = "Channel", school = "Shadow", ticks = 5, duration = 5, name = "Drain Life" },
    [1120] = { sp = 0.429, wd = 0, type = "Channel", school = "Shadow", ticks = 5, duration = 15, name = "Drain Soul" },
    [18265] = { sp = 0.8, wd = 0, type = "DoT", school = "Shadow", ticks = 10, duration = 30, name = "Siphon Life" },
    [30283] = { sp = 0.195, wd = 0, type = "Direct", school = "Shadow", name = "Shadowfury" },
    [17877] = { sp = 0.429, wd = 0, type = "Direct", school = "Shadow", name = "Shadowburn" },
    [6789] = { sp = 0.429, wd = 0, type = "Direct", school = "Shadow", name = "Death Coil" },
    [1949] = { sp = 0.286, wd = 0, type = "Channel", school = "Fire", ticks = 15, duration = 15, name = "Hellfire" },
    [5740] = { sp = 0.143, wd = 0, type = "Channel", school = "Fire", ticks = 4, duration = 8, name = "Rain of Fire" },
    
    -- ===================
    -- MAGE SPELLS
    -- ===================
    [133] = { sp = 1.0, wd = 0, type = "Direct", school = "Fire", name = "Fireball" },
    [2136] = { sp = 0.429, wd = 0, type = "Direct", school = "Fire", name = "Fire Blast" },
    [2948] = { sp = 0.429, wd = 0, type = "Direct", school = "Fire", name = "Scorch" },
    [11366] = { sp = 1.15, wd = 0, type = "Direct", school = "Fire", dotSp = 0.05, name = "Pyroblast" },
    [11113] = { sp = 0.143, wd = 0, type = "Direct", school = "Fire", name = "Blast Wave" },
    [2120] = { sp = 0.243, wd = 0, type = "Direct", school = "Fire", dotSp = 0.05, name = "Flamestrike" },
    [31661] = { sp = 0.143, wd = 0, type = "Direct", school = "Fire", name = "Dragon's Breath" },
    [116] = { sp = 0.814, wd = 0, type = "Direct", school = "Frost", name = "Frostbolt" },
    [120] = { sp = 0.135, wd = 0, type = "Direct", school = "Frost", name = "Cone of Cold" },
    [122] = { sp = 0.05, wd = 0, type = "Direct", school = "Frost", name = "Frost Nova" },
    [10] = { sp = 0.143, wd = 0, type = "Channel", school = "Frost", ticks = 8, duration = 8, name = "Blizzard" },
    [30455] = { sp = 0.143, wd = 0, type = "Direct", school = "Frost", name = "Ice Lance" },
    [11426] = { sp = 0.1, wd = 0, type = "Shield", school = "Frost", name = "Ice Barrier" },
    [5143] = { sp = 0.286, wd = 0, type = "Channel", school = "Arcane", ticks = 5, duration = 5, name = "Arcane Missiles" },
    [30451] = { sp = 0.714, wd = 0, type = "Direct", school = "Arcane", name = "Arcane Blast" },
    [1449] = { sp = 0.143, wd = 0, type = "Direct", school = "Arcane", name = "Arcane Explosion" },
    
    -- ===================
    -- PALADIN SPELLS  
    -- ===================
    [635] = { sp = 0.714, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Holy Light" },
    [19750] = { sp = 0.429, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Flash of Light" },
    [25914] = { sp = 0.429, wd = 0, type = "Direct", school = "Holy", isHeal = true, name = "Holy Shock" },
    [26573] = { sp = 0.33, wd = 0, type = "DoT", school = "Holy", ticks = 8, duration = 8, name = "Consecration" },
    [879] = { sp = 0.429, wd = 0, type = "Direct", school = "Holy", name = "Exorcism" },
    [24275] = { sp = 0.429, wd = 0, type = "Direct", school = "Holy", name = "Hammer of Wrath" },
    [31935] = { sp = 0.091, wd = 0, type = "Direct", school = "Holy", name = "Avenger's Shield" },
    [20187] = { sp = 0.71, wd = 0, type = "Direct", school = "Holy", name = "Judgement of Righteousness" },
    [20424] = { sp = 0.29, wd = 0, type = "Proc", school = "Holy", name = "Seal of Command" },
    [21084] = { sp = 0.1, wd = 0, type = "Proc", school = "Holy", name = "Seal of Righteousness" },
    [31801] = { sp = 0.013, wd = 0, type = "DoT", school = "Holy", name = "Seal of Vengeance" },
    [348704] = { sp = 0.22, wd = 0, type = "Proc", school = "Holy", name = "Seal of Martyrdom" },
    
    -- ===================
    -- DRUID SPELLS
    -- ===================
    [5176] = { sp = 0.571, wd = 0, type = "Direct", school = "Nature", name = "Wrath" },
    [2912] = { sp = 1.0, wd = 0, type = "Direct", school = "Arcane", name = "Starfire" },
    [8921] = { sp = 0.15, wd = 0, type = "Direct", school = "Arcane", dotSp = 0.52, dotTicks = 4, dotDuration = 12, name = "Moonfire" },
    [16914] = { sp = 0.129, wd = 0, type = "Channel", school = "Nature", ticks = 10, duration = 10, name = "Hurricane" },
    [5570] = { sp = 0.76, wd = 0, type = "DoT", school = "Nature", ticks = 6, duration = 12, name = "Insect Swarm" },
    [8936] = { sp = 0.3, wd = 0, type = "Direct", school = "Nature", isHeal = true, hotSp = 0.513, name = "Regrowth" },
    [774] = { sp = 0.8, wd = 0, type = "HoT", school = "Nature", isHeal = true, ticks = 4, duration = 12, name = "Rejuvenation" },
    [5185] = { sp = 1.611, wd = 0, type = "Direct", school = "Nature", isHeal = true, name = "Healing Touch" },
    [18562] = { sp = 1.0, wd = 0, type = "Direct", school = "Nature", isHeal = true, name = "Swiftmend" },
    [33763] = { sp = 0.286, wd = 0, type = "HoT", school = "Nature", isHeal = true, bloomSp = 0.343, name = "Lifebloom" },
    
    -- ===================
    -- SHAMAN SPELLS
    -- ===================
    [403] = { sp = 0.794, wd = 0, type = "Direct", school = "Nature", name = "Lightning Bolt" },
    [421] = { sp = 0.571, wd = 0, type = "Direct", school = "Nature", name = "Chain Lightning" },
    [8042] = { sp = 0.386, wd = 0, type = "Direct", school = "Nature", name = "Earth Shock" },
    [8050] = { sp = 0.15, wd = 0, type = "Direct", school = "Fire", dotSp = 0.52, dotTicks = 4, dotDuration = 12, name = "Flame Shock" },
    [8056] = { sp = 0.386, wd = 0, type = "Direct", school = "Frost", name = "Frost Shock" },
    [324] = { sp = 0.33, wd = 0, type = "Proc", school = "Nature", name = "Lightning Shield" },
    [331] = { sp = 0.857, wd = 0, type = "Direct", school = "Nature", isHeal = true, name = "Healing Wave" },
    [8004] = { sp = 0.429, wd = 0, type = "Direct", school = "Nature", isHeal = true, name = "Lesser Healing Wave" },
    [1064] = { sp = 0.714, wd = 0, type = "Direct", school = "Nature", isHeal = true, name = "Chain Heal" },
    [974] = { sp = 0.286, wd = 0, type = "Shield", school = "Nature", isHeal = true, name = "Earth Shield" },
    [5394] = { sp = 0.045, wd = 0, type = "HoT", school = "Nature", isHeal = true, name = "Healing Stream Totem" },
    [17364] = { sp = 0, wd = 1.0, type = "Melee", school = "Nature", name = "Stormstrike" },
    
    -- ===================
    -- HUNTER SPELLS
    -- ===================
    [19434] = { sp = 0, wd = 1.0, type = "Ranged", school = "Physical", rap = 0.2, name = "Aimed Shot" },
    [3044] = { sp = 0.15, wd = 0, type = "Ranged", school = "Arcane", rap = 0.15, name = "Arcane Shot" },
    [2643] = { sp = 0, wd = 1.0, type = "Ranged", school = "Physical", name = "Multi-Shot" },
    [1978] = { sp = 0.04, wd = 0, type = "DoT", school = "Nature", rap = 0.1, ticks = 5, duration = 15, name = "Serpent Sting" },
    [34490] = { sp = 0, wd = 0.5, type = "Ranged", school = "Physical", name = "Silencing Shot" },
    [53351] = { sp = 0, wd = 1.0, type = "Ranged", school = "Physical", rap = 0.4, name = "Kill Shot" },
    [27022] = { sp = 0.025, wd = 0, type = "DoT", school = "Fire", name = "Immolation Trap" },
    [13812] = { sp = 0.07, wd = 0, type = "Direct", school = "Fire", name = "Explosive Trap" },
    
    -- ===================
    -- ROGUE ABILITIES
    -- ===================
    [53] = { sp = 0, wd = 1.5, type = "Melee", school = "Physical", name = "Backstab" },
    [1752] = { sp = 0, wd = 1.0, type = "Melee", school = "Physical", name = "Sinister Strike" },
    [8676] = { sp = 0, wd = 2.5, type = "Melee", school = "Physical", name = "Ambush" },
    [2098] = { sp = 0, wd = 0.03, type = "Melee", school = "Physical", name = "Eviscerate" },
    [16511] = { sp = 0, wd = 1.1, type = "Melee", school = "Physical", name = "Hemorrhage" },
    
    -- ===================
    -- WARRIOR ABILITIES
    -- ===================
    [78] = { sp = 0, wd = 1.0, type = "Melee", school = "Physical", name = "Heroic Strike" },
    [845] = { sp = 0, wd = 1.0, type = "Melee", school = "Physical", name = "Cleave" },
    [12294] = { sp = 0, wd = 1.0, type = "Melee", school = "Physical", name = "Mortal Strike" },
    [23881] = { sp = 0, wd = 0.45, type = "Melee", school = "Physical", name = "Bloodthirst" },
    [20243] = { sp = 0, wd = 0.5, type = "Melee", school = "Physical", name = "Devastate" },
    [1464] = { sp = 0, wd = 1.0, type = "Melee", school = "Physical", name = "Slam" },
    [1680] = { sp = 0, wd = 1.0, type = "Melee", school = "Physical", name = "Whirlwind" },
    [7384] = { sp = 0, wd = 1.0, type = "Melee", school = "Physical", name = "Overpower" },
    [34428] = { sp = 0, wd = 0.45, type = "Melee", school = "Physical", name = "Victory Rush" },
    [57755] = { sp = 0, wd = 0.5, type = "Ranged", school = "Physical", name = "Heroic Throw" },
}

-- Spell school to stat index mapping
DT.SpellSchools = {
    ["Physical"] = 1,
    ["Holy"] = 2,
    ["Fire"] = 3,
    ["Nature"] = 4,
    ["Frost"] = 5,
    ["Shadow"] = 6,
    ["Arcane"] = 7,
}

-- ============================================================================
-- STAT RETRIEVAL FUNCTIONS (Retail Midnight Compatible)
-- Note: Retail unifies spell power/healing power into "Intellect + Spell Power"
-- ============================================================================

function DT.Math:GetSpellPower(school)
    local schoolIndex = DT.SpellSchools[school] or 2  -- Default to Holy
    
    -- Retail: GetSpellBonusDamage still exists and returns effective spell power
    if GetSpellBonusDamage then
        return GetSpellBonusDamage(schoolIndex) or 0
    end
    
    -- Fallback: Try to calculate from Intellect
    local _, intellect = UnitStat("player", 4)
    return (intellect or 0)
end

function DT.Math:GetHealingPower()
    -- Retail: Healing power equals spell power (unified system)
    if GetSpellBonusHealing then
        return GetSpellBonusHealing() or 0
    end
    
    -- Fallback: same as spell power
    return self:GetSpellPower("Holy")
end

function DT.Math:GetVersatility()
    -- Retail: Versatility affects all damage/healing
    if GetCombatRating and CR_VERSATILITY_DAMAGE_DONE then
        local rating = GetCombatRating(CR_VERSATILITY_DAMAGE_DONE) or 0
        local percent = GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) or 0
        return percent / 100
    end
    return 0
end

function DT.Math:GetMastery()
    -- Retail: Mastery effect varies by spec
    if GetMasteryEffect then
        local effect = GetMasteryEffect() or 0
        return effect / 100
    end
    return 0
end

function DT.Math:GetCritChance()
    -- Retail: Spell crit chance
    if GetSpellCritChance then
        local crit = GetSpellCritChance() or 0
        return crit / 100
    end
    return 0
end

function DT.Math:GetHaste()
    -- Retail: Haste percentage
    if UnitSpellHaste then
        return (UnitSpellHaste("player") or 0) / 100
    end
    return 0
end

function DT.Math:GetAttackPower()
    local base, posBuff, negBuff = UnitAttackPower("player")
    return (base or 0) + (posBuff or 0) + (negBuff or 0)
end

function DT.Math:GetRangedAttackPower()
    -- Retail: Ranged AP unified with melee AP
    local base, posBuff, negBuff = UnitAttackPower("player")
    return (base or 0) + (posBuff or 0) + (negBuff or 0)
end

function DT.Math:GetWeaponDamage(hand)
    if UnitDamage then
        local mainMin, mainMax, offMin, offMax = UnitDamage("player")
        if hand == "off" then
            return ((offMin or 0) + (offMax or 0)) / 2
        end
        return ((mainMin or 0) + (mainMax or 0)) / 2
    end
    return 0
end

-- Returns min, max weapon damage (for range display)
function DT.Math:GetWeaponDamageRange(hand)
    if UnitDamage then
        local mainMin, mainMax, offMin, offMax = UnitDamage("player")
        if hand == "off" then
            return (offMin or 0), (offMax or 0)
        end
        return (mainMin or 0), (mainMax or 0)
    end
    return 0, 0
end

function DT.Math:GetRangedWeaponDamage()
    if UnitRangedDamage then
        local speed, minDmg, maxDmg = UnitRangedDamage("player")
        return ((minDmg or 0) + (maxDmg or 0)) / 2
    end
    return 0
end

-- ============================================================================
-- PET STAT FUNCTIONS
-- Read pet damage/AP/SP for warlock demon abilities
-- ============================================================================

function DT.Math:GetPetDamage()
    -- Returns average melee damage of the player's pet
    if UnitDamage and UnitExists("pet") then
        local minDmg, maxDmg = UnitDamage("pet")
        return (minDmg or 0), (maxDmg or 0), ((minDmg or 0) + (maxDmg or 0)) / 2
    end
    return 0, 0, 0
end

function DT.Math:GetPetAttackPower()
    if UnitAttackPower and UnitExists("pet") then
        local base, posBuff, negBuff = UnitAttackPower("pet")
        return (base or 0) + (posBuff or 0) + (negBuff or 0)
    end
    return 0
end

function DT.Math:GetPetSpellPower()
    -- In TBC, pet spell power can be read via GetPetSpellBonusDamage()
    -- Fallback: estimate from warlock's own spell power (pets inherit ~15% base, more with talents)
    if GetPetSpellBonusDamage and UnitExists("pet") then
        return GetPetSpellBonusDamage() or 0
    end
    -- Fallback: try reading warlock's SP and estimate pet's share
    if GetSpellBonusDamage and UnitExists("pet") then
        local warlockSP = GetSpellBonusDamage(2) or 0  -- Holy index as baseline
        -- Max across all schools
        for i = 2, 7 do
            local v = GetSpellBonusDamage(i) or 0
            if v > warlockSP then warlockSP = v end
        end
        return math.floor(warlockSP * 0.15 + 0.5)  -- ~15% base inheritance
    end
    return 0
end

-- ============================================================================
-- NORMALIZED WEAPON DAMAGE
-- TBC weapon normalization speeds:
--   Daggers: 1.7, 1H: 2.4, 2H: 3.3, Ranged: 2.8
-- Normalized damage = WeaponDPS * NormalizedSpeed
-- Unnormalized damage = actual weapon damage (min/max from tooltip)
-- ============================================================================

DT.Math.NormSpeeds = {
    Dagger = 1.7,
    OneHand = 2.4,
    TwoHand = 3.3,
    Ranged = 2.8,
}

-- Spells that use UNNORMALIZED (actual) weapon damage
-- These use the weapon's real speed/damage, not normalized
DT.Math.UnnormalizedSpells = {
    ["Heroic Strike"] = true,
    ["Cleave"] = true,
    ["Aimed Shot"] = true,
    ["Slam"] = true,      -- Uses actual weapon damage
    ["Raptor Strike"] = true,
    ["Maul"] = true,
    ["Hemorrhage"] = true, -- One of the few unnormalized instant attacks
    -- Feral druid: fixed form weapon speeds (cat=1.0, bear=2.5), normalization N/A
    ["Shred"] = true,
    ["Mangle (Cat)"] = true,
    ["Mangle (Bear)"] = true,
    ["Ravage"] = true,
    ["Claw"] = true,
    -- Shoot (bow/gun/wand): pure weapon damage, not normalized
    ["Shoot"] = true,
}

-- Spells that use NORMALIZED weapon speed for damage calc
-- Most instant melee attacks are normalized
DT.Math.NormalizedSpells = {
    ["Sinister Strike"] = true,
    ["Backstab"] = true,
    ["Ambush"] = true,
    ["Mortal Strike"] = true,
    ["Whirlwind"] = true,
    ["Overpower"] = true,
    ["Mutilate"] = true,
    ["Multi-Shot"] = true,
    ["Stormstrike"] = true,
}

-- Get the weapon type for normalization speed lookup
function DT.Math:GetWeaponType(hand)
    if not GetInventoryItemLink then return "OneHand" end
    
    local slot = (hand == "ranged") and 18 or 16  -- 16=MainHand, 17=OffHand, 18=Ranged
    if hand == "off" then slot = 17 end
    
    local link = GetInventoryItemLink("player", slot)
    if not link then return "OneHand" end
    
    local _, _, _, _, _, _, subType = GetItemInfo(link)
    if not subType then return "OneHand" end
    
    subType = subType:lower()
    if subType:find("dagger") then return "Dagger" end
    if subType:find("two%-hand") or subType:find("staff") or subType:find("polearm") then return "TwoHand" end
    if subType:find("bow") or subType:find("gun") or subType:find("crossbow") or subType:find("thrown") then return "Ranged" end
    return "OneHand"
end

-- Get normalized weapon damage for a hand
function DT.Math:GetNormalizedWeaponDamage(hand)
    if not UnitDamage then return 0, 0 end
    
    local mainMin, mainMax, mainSpeed, offMin, offMax, offSpeed
    
    if hand == "ranged" then
        if UnitRangedDamage then
            local speed, minD, maxD = UnitRangedDamage("player")
            if speed and speed > 0 then
                local normSpeed = self.NormSpeeds.Ranged
                local dps = (minD or 0) / speed
                local normMin = dps * normSpeed
                dps = (maxD or 0) / speed
                local normMax = dps * normSpeed
                return (normMin + normMax) / 2, normMin, normMax, normSpeed
            end
        end
        return 0, 0, 0, 2.8
    end
    
    local mainMinD, mainMaxD, offMinD, offMaxD, _, _ = UnitDamage("player")
    
    local speed = UnitAttackSpeed and select(hand == "off" and 2 or 1, UnitAttackSpeed("player")) or 2.0
    if not speed or speed <= 0 then speed = 2.0 end
    
    local minD = hand == "off" and (offMinD or 0) or (mainMinD or 0)
    local maxD = hand == "off" and (offMaxD or 0) or (mainMaxD or 0)
    
    local weaponType = self:GetWeaponType(hand)
    local normSpeed = self.NormSpeeds[weaponType] or 2.4
    local normMin = (minD / speed) * normSpeed
    local normMax = (maxD / speed) * normSpeed
    
    return (normMin + normMax) / 2, normMin, normMax, normSpeed
end

-- Determine if a spell uses normalized or unnormalized weapon damage
function DT.Math:IsNormalized(spellName)
    if self.NormalizedSpells[spellName] then return true end
    if self.UnnormalizedSpells[spellName] then return false end
    return nil  -- Unknown
end

-- Get base weapon speed (unaffected by haste) by scanning the weapon tooltip
-- Returns baseSpeed or nil if no weapon equipped
function DT.Math:GetBaseWeaponSpeed(hand)
    local slotID = (hand == "off") and 17 or 16  -- 16 = main hand, 17 = off hand
    local link = GetInventoryItemLink("player", slotID)
    if not link then
        if DT.DB and DT.DB.debugMode then print("|cffff9900[DT Debug]|r GetBaseWeaponSpeed: no item link for slot", slotID) end
        return nil
    end

    -- Create or reuse a hidden scanning tooltip
    local scanTip = _G["DT_WeaponScanTip"]
    if not scanTip then
        scanTip = CreateFrame("GameTooltip", "DT_WeaponScanTip", nil, "GameTooltipTemplate")
        scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
    end
    scanTip:ClearLines()
    scanTip:SetHyperlink(link)

    if DT.DB and DT.DB.debugMode then print("|cffff9900[DT Debug]|r GetBaseWeaponSpeed: scanning", scanTip:NumLines(), "lines for link:", link) end
    for i = 1, scanTip:NumLines() do
        -- Speed is shown on the right side of the tooltip
        local rightText = _G["DT_WeaponScanTipTextRight" .. i]
        if rightText then
            local text = rightText:GetText() or ""
            if DT.DB and DT.DB.debugMode and text ~= "" then print("|cffff9900[DT Debug]|r   Right line", i, ":", text) end
            local speed = text:match("Speed%s+(%d+%.?%d*)")
            if speed then
                scanTip:Hide()
                return tonumber(speed)
            end
        end
    end
    if DT.DB and DT.DB.debugMode then print("|cffff9900[DT Debug]|r GetBaseWeaponSpeed: NO speed found in tooltip lines, falling back to UnitAttackSpeed") end
    scanTip:Hide()
    -- Fallback: use UnitAttackSpeed (haste-modified, but better than nil)
    if UnitAttackSpeed then
        local mhSpeed, ohSpeed = UnitAttackSpeed("player")
        local fallback = (hand == "off") and ohSpeed or mhSpeed
        if fallback and fallback > 0 then
            if DT.DB and DT.DB.debugMode then print("|cffff9900[DT Debug]|r  UnitAttackSpeed fallback:", fallback) end
            return fallback
        end
    end
    return nil
end

-- ============================================================================
-- CALCULATION FUNCTIONS
-- ============================================================================

function DT.Math:CalculateSpellBonus(spellID)
    local data = DT.SpellData[spellID]
    if not data then return nil end
    
    local result = {
        spellID = spellID,
        name = data.name or "Unknown",
        school = data.school or "Physical",
        type = data.type or "Direct",
        isHeal = data.isHeal or false,
        coefficient = data.sp or 0,
        weaponCoeff = data.wd or 0,
        ticks = data.ticks,
        duration = data.duration,
    }
    
    -- Get appropriate stat
    local statValue = 0
    local statName = ""
    
    if data.isHeal then
        statValue = self:GetHealingPower()
        statName = "Healing Power"
    elseif data.sp > 0 then
        statValue = self:GetSpellPower(data.school)
        statName = "Spell Power"
    elseif data.wd > 0 then
        if data.type == "Ranged" then
            statValue = self:GetRangedWeaponDamage()
            statName = "Ranged Weapon"
        else
            statValue = self:GetWeaponDamage("main")
            statName = "Weapon Damage"
        end
    end
    
    -- Calculate bonus from spell power / healing power
    if data.sp > 0 then
        result.spBonus = statValue * data.sp
        result.statValue = statValue
        result.statName = statName
    end
    
    -- Calculate bonus from weapon damage
    if data.wd > 0 then
        result.wdBonus = statValue * data.wd
        result.statValue = statValue
        result.statName = statName
    end
    
    -- Handle RAP for hunters
    if data.rap then
        local rap = self:GetRangedAttackPower()
        result.rapBonus = rap * data.rap
        result.rapValue = rap
    end
    
    -- Handle DoT portion if exists
    if data.dotSp then
        local sp = data.isHeal and self:GetHealingPower() or self:GetSpellPower(data.school)
        result.dotBonus = sp * data.dotSp
        result.dotTicks = data.dotTicks
        result.dotDuration = data.dotDuration
    end
    
    return result
end

function DT.Math:FormatNumber(num)
    if not num then return "0" end
    num = math.floor(num + 0.5)
    if num >= 1000000 then
        return string.format("%.1fM", num / 1000000)
    elseif num >= 1000 then
        return string.format("%.1fk", num / 1000)
    end
    return tostring(num)
end

function DT.Math:FormatDecimal(num, decimals)
    decimals = decimals or 1
    return string.format("%." .. decimals .. "f", num or 0)
end

-- ============================================================================
-- TOOLTIP TEXT GENERATION
-- ============================================================================

function DT.Math:GenerateTooltipLines(spellID, baseDamageMin, baseDamageMax)
    local calc = self:CalculateSpellBonus(spellID)
    if not calc then return nil end
    
    local lines = {}
    local c = DT.Style.colors
    
    -- Separator line
    table.insert(lines, {
        text = " ",
        r = 1, g = 1, b = 1
    })
    
    -- Calculate total bonus
    local totalBonus = 0
    local coeffPercent = 0
    local statUsed = ""
    
    if calc.spBonus and calc.spBonus > 0 then
        totalBonus = calc.spBonus
        coeffPercent = calc.coefficient * 100
        statUsed = calc.statName
    elseif calc.wdBonus and calc.wdBonus > 0 then
        totalBonus = calc.wdBonus
        coeffPercent = calc.weaponCoeff * 100
        statUsed = calc.statName
    end
    
    -- Add RAP bonus if applicable
    if calc.rapBonus and calc.rapBonus > 0 then
        totalBonus = totalBonus + calc.rapBonus
    end
    
    -- Format base damage display
    local baseDmg = 0
    local baseDmgText = ""
    if baseDamageMin and baseDamageMax then
        if baseDamageMin == baseDamageMax then
            baseDmg = baseDamageMin
            baseDmgText = tostring(baseDamageMin)
        else
            baseDmg = (baseDamageMin + baseDamageMax) / 2
            baseDmgText = baseDamageMin .. "-" .. baseDamageMax
        end
    end
    
    -- Line 1: Base + Coefficient source
    local actionWord = calc.isHeal and "Healing" or "Damage"
    if calc.type == "Shield" then actionWord = "Absorb" end
    
    local coeffColor = "|cffffd100"  -- Gold
    local bonusColor = calc.isHeal and "|cff00ff00" or "|cff66d9ff"  -- Green for heals, cyan for damage
    
    if coeffPercent > 0 then
        local line1 = string.format("Base %s: %s + %s%.0f%%|r of %s",
            actionWord,
            baseDmgText ~= "" and baseDmgText or "???",
            coeffColor,
            coeffPercent,
            statUsed
        )
        table.insert(lines, {
            text = line1,
            r = c.description.r, g = c.description.g, b = c.description.b
        })
    end
    
    -- Line 2: Total calculation breakdown
    if baseDmg > 0 and totalBonus > 0 then
        local totalDmg = baseDmg + totalBonus
        local calcText = string.format("Total %s: %s + %s(%s * %.2f)|r",
            actionWord,
            baseDmgText,
            bonusColor,
            self:FormatNumber(calc.statValue or 0),
            calc.coefficient or calc.weaponCoeff or 0
        )
        table.insert(lines, {
            text = calcText,
            r = c.stats.r, g = c.stats.g, b = c.stats.b
        })
        
        -- Line 3: Final result
        local resultText = string.format("  = %s%s|r %s",
            bonusColor,
            self:FormatNumber(totalDmg),
            calc.school .. " " .. actionWord:lower()
        )
        table.insert(lines, {
            text = resultText,
            r = c.stats.r, g = c.stats.g, b = c.stats.b
        })
    elseif totalBonus > 0 then
        -- Just show the bonus without base
        local bonusText = string.format("%s+%s|r from %s (%.0f%% coeff)",
            bonusColor,
            self:FormatNumber(totalBonus),
            statUsed,
            coeffPercent
        )
        table.insert(lines, {
            text = bonusText,
            r = c.coeffBonus.r, g = c.coeffBonus.g, b = c.coeffBonus.b
        })
    end
    
    -- DoT/HoT specific info
    if calc.ticks and calc.duration then
        local perTick = totalBonus / calc.ticks
        local dps = (baseDmg + totalBonus) / calc.duration
        
        table.insert(lines, {
            text = " ",
            r = 1, g = 1, b = 1
        })
        
        local actionPerSec = calc.isHeal and "Healing per Second" or "Damage per Second"
        local tickText = string.format("Deals %s%s|r %s over %ds.",
            bonusColor,
            self:FormatNumber(baseDmg + totalBonus),
            calc.school:lower() .. " " .. (calc.isHeal and "healing" or "damage"),
            calc.duration
        )
        table.insert(lines, {
            text = tickText,
            r = c.description.r, g = c.description.g, b = c.description.b
        })
        
        local dpsText = string.format("(%s%.1f %s|r)",
            "|cffffff00",  -- Yellow
            dps,
            calc.isHeal and "HPS" or "DPS"
        )
        table.insert(lines, {
            text = dpsText,
            r = c.footer.r, g = c.footer.g, b = c.footer.b
        })
    end
    
    -- DoT portion for spells with direct + DoT
    if calc.dotBonus and calc.dotBonus > 0 then
        table.insert(lines, {
            text = " ",
            r = 1, g = 1, b = 1
        })
        table.insert(lines, {
            text = string.format("DoT: %s+%s|r from Spell Power",
                bonusColor,
                self:FormatNumber(calc.dotBonus)
            ),
            r = c.stats.r, g = c.stats.g, b = c.stats.b
        })
    end
    
    return lines
end

-- ============================================================================
-- THREAT DATA
-- Bonus threat values for abilities with built-in threat modifiers
-- Format: [spellName] = { flat = flatBonusThreat, multi = threatMultiplier }
-- flat: added directly on top of damage threat
-- multi: multiplies the total threat (e.g. 1.3 = 30% more threat than damage)
-- ============================================================================

DT.ThreatData = {
    -- WARRIOR (Defensive Stance base = 1.3x, Battle/Berserker = 0.8x)
    ["Heroic Strike"]       = { flat = 145, multi = 1.0 },  -- Rank 11 flat bonus
    ["Shield Slam"]         = { flat = 250, multi = 1.0 },  -- High flat bonus
    ["Revenge"]             = { flat = 200, multi = 1.0 },  -- Flat threat bonus
    ["Devastate"]           = { flat = 100, multi = 1.0 },  -- Per Sunder application
    ["Sunder Armor"]        = { flat = 301, multi = 1.0 },  -- Pure threat, no damage
    ["Shield Bash"]         = { flat = 230, multi = 1.0 },
    ["Thunder Clap"]        = { flat = 175, multi = 1.0 },  -- AoE threat
    ["Taunt"]               = { flat = 0, multi = 0, note = "Sets threat to highest" },
    ["Mocking Blow"]        = { flat = 290, multi = 1.0 },
    ["Challenging Shout"]   = { flat = 0, multi = 0, note = "AoE taunt 6 sec" },
    ["Cleave"]              = { flat = 100, multi = 1.0 },
    ["Demoralizing Shout"]  = { flat = 56, multi = 1.0 },
    ["Battle Shout"]        = { flat = 69, multi = 1.0 },  -- Generates threat per buffed target
    ["Commanding Shout"]    = { flat = 68, multi = 1.0 },
    -- PALADIN
    ["Holy Light"]          = { flat = 0, multi = 0.5 },   -- 0.5x threat (healing)
    ["Flash of Light"]      = { flat = 0, multi = 0.5 },
    ["Consecration"]        = { flat = 0, multi = 1.0 },   -- No modifier but generates good threat
    ["Avenger's Shield"]    = { flat = 0, multi = 1.0 },   -- Righteous Fury makes it high threat
    ["Holy Shield"]         = { flat = 0, multi = 1.35 },  -- 35% additional threat (Imp HS, RF handled dynamically)
    -- DRUID (Bear Form = 1.3x with Feral Instinct)
    ["Maul"]                = { flat = 176, multi = 1.0 },  -- Flat threat bonus in bear
    ["Swipe"]               = { flat = 0, multi = 1.0 },
    ["Lacerate"]            = { flat = 285, multi = 1.0 },
    ["Mangle (Bear)"]       = { flat = 0, multi = 1.3 },
    -- ROGUE
    ["Feint"]               = { flat = -600, multi = 1.0, note = "Reduces threat" },
    ["Vanish"]              = { flat = 0, multi = 0, note = "Drops all threat" },
    -- MAGE
    ["Arcane Explosion"]    = { flat = 0, multi = 0.75 },  -- Reduced threat
    ["Frostbolt"]           = { flat = 0, multi = 0.9 },   -- Frost Channeling talent reduces threat
}

-- Export
_G.DynamicTooltipsMath = DT.Math

-- ============================================================================
-- TALENT & BUFF MODIFIER SYSTEM
-- Scans player talents BY NAME (no hardcoded tab/index) and active buffs
-- that modify spell damage/healing output.
-- ============================================================================

DT.Modifiers = {}

-- ────────────────────────────────────────────────────────────────────────────
-- TALENT DATABASE
-- perRank: fractional bonus per talent rank (0.02 = 2% per point)
-- modType: determines which spells this modifier applies to
--   "all"            All damage and healing
--   "heal"           All healing spells
--   "spell_damage"   Non-healing spell damage (any magic school)
--   "shadow"         Shadow school damage
--   "fire"           Fire school
--   "frost"          Frost school
--   "nature"         Nature school
--   "arcane"         Arcane school
--   "holy_damage"    Holy school damage (not healing)
--   "arcane_nature"  Arcane or Nature damage
--   "physical"       Physical / melee damage (wd or ap based)
--   "ranged"         Ranged attacks (type = "Ranged")
--   "pet_melee"      Pet melee attacks
--   "spell_specific" Only spells listed in affectsSpells
-- affectsSpells: (spell_specific only) table of exact spell names
-- ────────────────────────────────────────────────────────────────────────────
DT.Modifiers.TalentDB = {

    -- ════════════════════════════════════════════════════════════════════════
    -- PRIEST
    -- ════════════════════════════════════════════════════════════════════════
    -- Discipline
    { name = "Focused Power",       perRank = 0.02, modType = "all",            class = "PRIEST" },
    { name = "Force of Will",       perRank = 0.01, modType = "spell_damage",   class = "PRIEST" },
    -- Holy
    { name = "Searing Light",       perRank = 0.05, modType = "spell_specific", class = "PRIEST",
      affectsSpells = { "Smite", "Holy Fire" } },
    { name = "Spiritual Healing",   perRank = 0.02, modType = "heal",           class = "PRIEST" },
    -- Shadow
    { name = "Darkness",            perRank = 0.02, modType = "shadow",         class = "PRIEST" },

    -- ════════════════════════════════════════════════════════════════════════
    -- MAGE
    -- ════════════════════════════════════════════════════════════════════════
    -- Arcane
    { name = "Arcane Instability",  perRank = 0.01, modType = "spell_damage",   class = "MAGE" },
    -- Fire
    { name = "Playing with Fire",   perRank = 0.01, modType = "spell_damage",   class = "MAGE" },
    { name = "Fire Power",          perRank = 0.02, modType = "fire",           class = "MAGE" },
    -- Frost
    { name = "Piercing Ice",        perRank = 0.02, modType = "frost",          class = "MAGE" },
    { name = "Arctic Winds",        perRank = 0.01, modType = "frost",          class = "MAGE" },

    -- ════════════════════════════════════════════════════════════════════════
    -- WARLOCK
    -- ════════════════════════════════════════════════════════════════════════
    -- Affliction
    { name = "Shadow Mastery",      perRank = 0.02, modType = "shadow",         class = "WARLOCK" },
    { name = "Contagion",           perRank = 0.01, modType = "spell_specific", class = "WARLOCK",
      affectsSpells = { "Corruption", "Siphon Life", "Seed of Corruption" } },
    -- Destruction
    { name = "Emberstorm",          perRank = 0.02, modType = "fire",           class = "WARLOCK" },

    -- ════════════════════════════════════════════════════════════════════════
    -- DRUID
    -- ════════════════════════════════════════════════════════════════════════
    -- Balance
    { name = "Moonfury",            perRank = 0.02, modType = "spell_specific", class = "DRUID",
      affectsSpells = { "Starfire", "Moonfire", "Wrath" } },
    { name = "Improved Moonfire",   perRank = 0.05, modType = "spell_specific", class = "DRUID",
      affectsSpells = { "Moonfire" } },
    -- Feral
    { name = "Feral Instinct",      perRank = 0.05, modType = "threat_bear",    class = "DRUID" },
    { name = "Naturalist",          perRank = 0.02, modType = "physical",       class = "DRUID" },
    { name = "Savage Fury",         perRank = 0.10, modType = "spell_specific", class = "DRUID",
      affectsSpells = { "Claw", "Rake", "Mangle", "Mangle (Cat)", "Mangle (Bear)" } },
    { name = "Feral Aggression",    perRank = 0.03, modType = "spell_specific", class = "DRUID",
      affectsSpells = { "Ferocious Bite" } },
    -- Restoration
    { name = "Gift of Nature",           perRank = 0.02, modType = "heal",           class = "DRUID" },
    { name = "Improved Rejuvenation",    perRank = 0.05, modType = "spell_specific", class = "DRUID",
      affectsSpells = { "Rejuvenation" } },

    -- ════════════════════════════════════════════════════════════════════════
    -- PALADIN
    -- ════════════════════════════════════════════════════════════════════════
    -- Holy
    { name = "Healing Light",       perRank = 0.04, modType = "heal",           class = "PALADIN" },
    { name = "Improved Seal of Righteousness", perRank = 0.03, modType = "spell_specific", class = "PALADIN",
      affectsSpells = { "Seal of Righteousness" }, baseDmgOnly = true },
    -- Protection
    { name = "Improved Holy Shield", perRank = 0.10, modType = "spell_specific", class = "PALADIN",
      affectsSpells = { "Holy Shield" } },
    { name = "One-Handed Weapon Specialization", perRank = 0.01, modType = "seal_and_melee", class = "PALADIN" },

    -- ════════════════════════════════════════════════════════════════════════
    -- SHAMAN
    -- ════════════════════════════════════════════════════════════════════════
    -- Elemental
    { name = "Concussion",          perRank = 0.01, modType = "spell_specific", class = "SHAMAN",
      affectsSpells = { "Lightning Bolt", "Chain Lightning", "Earth Shock", "Flame Shock", "Frost Shock" } },
    { name = "Call of Flame",       perRank = 0.05, modType = "spell_specific", class = "SHAMAN",
      affectsSpells = { "Fire Nova Totem", "Magma Totem", "Searing Totem" } },
    -- Restoration
    { name = "Purification",        perRank = 0.02, modType = "heal",           class = "SHAMAN" },
    { name = "Improved Chain Heal", perRank = 0.10, modType = "spell_specific", class = "SHAMAN",
      affectsSpells = { "Chain Heal" } },

    -- ════════════════════════════════════════════════════════════════════════
    -- HUNTER
    -- ════════════════════════════════════════════════════════════════════════
    -- Beast Mastery
    { name = "Focused Fire",        perRank = 0.01, modType = "all",            class = "HUNTER" },
    { name = "Unleashed Fury",      perRank = 0.04, modType = "pet_melee",      class = "HUNTER" },
    -- Marksmanship
    { name = "Ranged Weapon Specialization", perRank = 0.01, modType = "ranged", class = "HUNTER" },
    { name = "Barrage",             perRank = 0.04, modType = "spell_specific", class = "HUNTER",
      affectsSpells = { "Aimed Shot", "Multi-Shot", "Volley" } },

    -- ════════════════════════════════════════════════════════════════════════
    -- ROGUE
    -- ════════════════════════════════════════════════════════════════════════
    -- Combat
    { name = "Aggression",          perRank = 0.02, modType = "spell_specific", class = "ROGUE",
      affectsSpells = { "Sinister Strike", "Backstab", "Eviscerate" } },
    { name = "Surprise Attacks",    perRank = 0.10, modType = "spell_specific", class = "ROGUE",
      affectsSpells = { "Sinister Strike", "Backstab", "Hemorrhage", "Shiv" } },
    -- Subtlety
    { name = "Opportunity",         perRank = 0.04, modType = "spell_specific", class = "ROGUE",
      affectsSpells = { "Backstab", "Mutilate", "Garrote", "Ambush" } },

    -- ════════════════════════════════════════════════════════════════════════
    -- WARRIOR
    -- ════════════════════════════════════════════════════════════════════════
    -- Protection
    { name = "Defiance", perRank = 0.03, modType = "threat_defensive", class = "WARRIOR" },
}

-- ────────────────────────────────────────────────────────────────────────────
-- BUFF DATABASE — active player buffs that modify damage / healing output
-- ────────────────────────────────────────────────────────────────────────────
DT.Modifiers.BuffDB = {
    { name = "Shadowform",      spellID = 15473, modifier = 0.15, modType = "shadow",   class = "PRIEST" },
    { name = "Arcane Power",    spellID = 12042, modifier = 0.30, modType = "all",      class = "MAGE" },
    { name = "Power Infusion",  spellID = 10060, modifier = 0.20, modType = "all",      class = "PRIEST" },
    { name = "Death Wish",      spellID = 12292, modifier = 0.20, modType = "physical", class = "WARRIOR" },
    -- Threat multipliers (not damage, used by threat display only)
    { name = "Righteous Fury",   spellID = 25780, modifier = 0.90, modType = "threat_holy", class = "PALADIN" },
}

-- ────────────────────────────────────────────────────────────────────────────
-- TALENT CACHE — invalidated on talent-change events
-- ────────────────────────────────────────────────────────────────────────────
local talentCache = nil

local modEventFrame = CreateFrame("Frame")
modEventFrame:RegisterEvent("CHARACTER_POINTS_CHANGED")
modEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
-- TBC Anniversary may support dual-spec events; register safely
pcall(function() modEventFrame:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED") end)
pcall(function() modEventFrame:RegisterEvent("PLAYER_TALENT_UPDATE") end)
modEventFrame:SetScript("OnEvent", function()
    talentCache = nil
end)

function DT.Modifiers:InvalidateCache()
    talentCache = nil
end

-- ────────────────────────────────────────────────────────────────────────────
-- TALENT SCANNER — scans ALL talent tabs by NAME (no hardcoded tab/index)
-- Results are cached until talents change.
-- ────────────────────────────────────────────────────────────────────────────
function DT.Modifiers:GetTalentModifiers()
    if talentCache then return talentCache end

    local modifiers = {}
    local _, playerClass = UnitClass("player")

    if not GetNumTalentTabs or not GetNumTalents or not GetTalentInfo then
        return modifiers
    end

    -- Build name → { rank, maxRank } lookup from the live talent tree
    local playerTalents = {}
    for tab = 1, GetNumTalentTabs() do
        for index = 1, GetNumTalents(tab) do
            local name, _, _, _, currentRank, maxRank = GetTalentInfo(tab, index)
            if name and currentRank and currentRank > 0 then
                playerTalents[name] = { rank = currentRank, maxRank = maxRank }
            end
        end
    end

    -- Match scanned talents against our database
    for _, talent in ipairs(self.TalentDB) do
        if talent.class == playerClass then
            local info = playerTalents[talent.name]
            if info then
                table.insert(modifiers, {
                    name = talent.name,
                    modifier = info.rank * talent.perRank,
                    modType = talent.modType,
                    rank = info.rank,
                    maxRank = info.maxRank,
                    affectsSpells = talent.affectsSpells,
                    baseDmgOnly = talent.baseDmgOnly,
                    source = "talent",
                })
            end
        end
    end

    talentCache = modifiers
    return modifiers
end

-- ────────────────────────────────────────────────────────────────────────────
-- BUFF SCANNER — checks active player buffs against our database
-- ────────────────────────────────────────────────────────────────────────────
function DT.Modifiers:GetBuffModifiers()
    local modifiers = {}
    if not UnitBuff then return modifiers end

    local activeBuffs = {}
    for i = 1, 40 do
        local name = UnitBuff("player", i)
        if not name then break end
        activeBuffs[name] = true
    end

    for _, buff in ipairs(self.BuffDB) do
        if activeBuffs[buff.name] then
            table.insert(modifiers, {
                name = buff.name,
                modifier = buff.modifier,
                modType = buff.modType,
                source = "buff",
            })
        end
    end

    return modifiers
end

-- ────────────────────────────────────────────────────────────────────────────
-- APPLIES-TO — checks whether a modifier applies to the given spell data
-- ────────────────────────────────────────────────────────────────────────────
function DT.Modifiers:AppliesTo(mod, data)
    if not mod or not data then return false end

    local mt = mod.modType
    local school = (data.school or ""):lower()
    local isHeal = data.isHeal
    local spellName = data.name or ""

    -- ── Spell-specific: only match named spells ────────────────────────
    if mt == "spell_specific" then
        if mod.affectsSpells then
            for _, name in ipairs(mod.affectsSpells) do
                if name == spellName then return true end
            end
        end
        return false
    end

    -- ── Universal ──────────────────────────────────────────────────────
    if mt == "all" then return true end

    -- ── Healing only ───────────────────────────────────────────────────
    if mt == "heal" and isHeal then return true end

    -- ── Spell damage (non-healing, non-physical magic, non-shield) ────
    if mt == "spell_damage" and not isHeal and data.type ~= "Shield" then
        if school == "shadow" or school == "fire" or school == "frost"
           or school == "nature" or school == "arcane" or school == "holy" then
            return true
        end
        if data.sp and data.sp > 0 then return true end
        return false
    end

    -- ── Physical / melee damage ────────────────────────────────────────
    if mt == "physical" and not isHeal then
        if school == "physical" then return true end
        if data.wd and data.wd > 0 then return true end
        if data.ap and data.ap > 0 then return true end
        return false
    end

    -- ── Seal & melee (One-Handed Weapon Specialization) ────────────────
    if mt == "seal_and_melee" and not isHeal then
        if data.type == "Proc" or data.type == "Enchant" then return true end
        if school == "physical" then return true end
        if data.wd and data.wd > 0 then return true end
        if data.ap and data.ap > 0 then return true end
        return false
    end

    -- ── Ranged attacks ─────────────────────────────────────────────────
    if mt == "ranged" then return data.type == "Ranged" end

    -- ── Pet melee ──────────────────────────────────────────────────────
    if mt == "pet_melee" then
        return data.isPet == true and data.wd and data.wd > 0
    end

    -- ── School-specific (damage, not healing/shields) ────────────────
    local isShield = data.type == "Shield"
    if mt == "shadow"      and school == "shadow"  and not isHeal and not isShield then return true end
    if mt == "fire"        and school == "fire"    and not isShield                then return true end
    if mt == "frost"       and school == "frost"   and not isHeal and not isShield then return true end
    if mt == "nature"      and school == "nature"  and not isShield                then return true end
    if mt == "arcane"      and school == "arcane"  and not isShield                then return true end
    if mt == "holy_damage" and school == "holy"    and not isHeal and not isShield then return true end

    -- ── Multi-school ───────────────────────────────────────────────────
    if mt == "arcane_nature" and (school == "arcane" or school == "nature") and not isHeal and not isShield then return true end
    if mt == "nature_fire"   and (school == "nature" or school == "fire")   and not isShield               then return true end
    if mt == "fire_frost"    and (school == "fire"   or school == "frost")  and not isShield               then return true end
    if mt == "shadow_arcane" and (school == "shadow" or school == "arcane") and not isShield               then return true end

    return false
end

-- ────────────────────────────────────────────────────────────────────────────
-- COMBINED MODIFIER — total modifier from all active talents + buffs
-- Returns: totalMod (number), appliedMods (table of { name, modifier, source }),
--          baseMod (number — sum of baseDmgOnly modifiers, applied to base damage only)
-- ────────────────────────────────────────────────────────────────────────────
function DT.Modifiers:GetCombinedModifier(data)
    local totalMod = 0
    local baseMod = 0
    local appliedMods = {}

    local talentMods = self:GetTalentModifiers()
    local buffMods   = self:GetBuffModifiers()

    for _, mod in ipairs(talentMods) do
        if self:AppliesTo(mod, data) then
            if mod.baseDmgOnly then
                baseMod = baseMod + mod.modifier
            else
                totalMod = totalMod + mod.modifier
            end
            table.insert(appliedMods, {
                name = mod.name,
                modifier = mod.modifier,
                source = "talent",
                rank = mod.rank,
                maxRank = mod.maxRank,
                baseDmgOnly = mod.baseDmgOnly,
            })
        end
    end

    for _, mod in ipairs(buffMods) do
        if self:AppliesTo(mod, data) then
            if mod.baseDmgOnly then
                baseMod = baseMod + mod.modifier
            else
                totalMod = totalMod + mod.modifier
            end
            table.insert(appliedMods, {
                name = mod.name,
                modifier = mod.modifier,
                source = "buff",
                baseDmgOnly = mod.baseDmgOnly,
            })
        end
    end

    return totalMod, appliedMods, baseMod
end

-- ────────────────────────────────────────────────────────────────────────────
-- THREAT MODIFIER — returns extra threat multiplier from buffs/stances
-- Handles: Righteous Fury (threat_holy), Warrior stances, Defiance talent
-- Returns: threatMod (number, multiplicative), appliedThreatMods (table)
-- ────────────────────────────────────────────────────────────────────────────
function DT.Modifiers:GetThreatModifier(data)
    local threatMultiplier = 1.0
    local appliedThreatMods = {}

    if not data then return 0, appliedThreatMods end
    local school = (data.school or ""):lower()
    local _, playerClass = UnitClass("player")

    -- Righteous Fury: +90% threat to holy spells
    local buffMods = self:GetBuffModifiers()
    for _, mod in ipairs(buffMods) do
        if mod.modType == "threat_holy" and school == "holy" then
            threatMultiplier = threatMultiplier * (1 + mod.modifier)
            table.insert(appliedThreatMods, {
                name = mod.name,
                modifier = mod.modifier,
                source = "buff",
            })
        end
    end

    -- Warrior stance threat multipliers
    if playerClass == "WARRIOR" and GetShapeshiftForm then
        local stanceIndex = GetShapeshiftForm()
        -- 1 = Battle Stance (0.8x), 2 = Defensive Stance (1.3x), 3 = Berserker Stance (0.8x)
        if stanceIndex == 2 then
            -- Defensive Stance: 1.3x base
            local stanceMod = 0.30
            threatMultiplier = threatMultiplier * (1 + stanceMod)
            table.insert(appliedThreatMods, {
                name = "Defensive Stance",
                modifier = stanceMod,
                source = "stance",
            })
            -- Defiance talent: +3% per rank (additive with stance, multiplicative on total)
            local talentMods = self:GetTalentModifiers()
            for _, mod in ipairs(talentMods) do
                if mod.modType == "threat_defensive" then
                    threatMultiplier = threatMultiplier * (1 + mod.modifier)
                    table.insert(appliedThreatMods, {
                        name = mod.name,
                        modifier = mod.modifier,
                        source = "talent",
                    })
                    break
                end
            end
        elseif stanceIndex == 1 or stanceIndex == 3 then
            -- Battle/Berserker Stance: 0.8x
            local stanceMod = -0.20
            threatMultiplier = threatMultiplier * (1 + stanceMod)
            local stanceName = stanceIndex == 1 and "Battle Stance" or "Berserker Stance"
            table.insert(appliedThreatMods, {
                name = stanceName,
                modifier = stanceMod,
                source = "stance",
            })
        end
    end

    -- Druid Bear Form: 1.3x base threat + Feral Instinct talent
    if playerClass == "DRUID" and GetShapeshiftForm then
        local formIndex = GetShapeshiftForm()
        -- In TBC: 1 = Bear/Dire Bear Form
        if formIndex == 1 then
            local bearMod = 0.30
            threatMultiplier = threatMultiplier * (1 + bearMod)
            table.insert(appliedThreatMods, {
                name = "Bear Form",
                modifier = bearMod,
                source = "stance",
            })
            -- Feral Instinct: +5% per rank threat in bear form
            local talentMods = self:GetTalentModifiers()
            for _, mod in ipairs(talentMods) do
                if mod.modType == "threat_bear" then
                    threatMultiplier = threatMultiplier * (1 + mod.modifier)
                    table.insert(appliedThreatMods, {
                        name = mod.name,
                        modifier = mod.modifier,
                        source = "talent",
                    })
                    break
                end
            end
        end
    end

    -- Convert multiplier to additive form: e.g. 1.3 → 0.3, 0.8 → -0.2
    local threatMod = threatMultiplier - 1.0
    return threatMod, appliedThreatMods
end
