--[[--------------------------------------------------------------------------
	Boom - Modern Healing & Support Raid Frames
	Data/AuraData.lua

	Curated knowledge about debuffs: what a healer must see, and what is noise.

	THE THREE-TIER MODEL
	  1. BOSS AURAS      the game itself flags encounter mechanics through
	                     auraData.isBossAura. Always important, no curation
	                     needed, and it keeps working in content released long
	                     after this file was written.
	  2. DISPELLABLE     anything the player can personally remove is surfaced by
	                     Engine/Auras.lua regardless of what is listed here.
	  3. CURATED LIST    importantDebuffs / crowdControl below - the effects the
	                     game does not flag but a healer still has to react to.
	  4. EVERYTHING ELSE hidden. Three legible icons beat twelve unreadable ones;
	                     an unfiltered debuff row is the reason people turn raid
	                     frame debuffs off entirely.

	Tiers 1 and 2 are the safety net, and that is what makes tier 3 safe to leave
	incomplete. A missing curated entry costs sort order, not visibility. An
	INVENTED spell ID costs trust: it silently marks the wrong aura important, or
	blacklists something that mattered. So every ID below was verified by name
	against the 12.0.7 client spell table before it was written down, and the
	gaps - notably current dungeon and raid mechanics, whose IDs churn every
	patch - are deliberately left as gaps for tiers 1 and 2 to cover.

	Users extend all three tables at runtime from the Auras options panel, which
	writes into Boom.profile.auras.extraImportant and .blacklist.
----------------------------------------------------------------------------]]

local ADDON_NAME, Boom = ...

local AuraData = Boom:NewModule("AuraData")

local L    = Boom.L
local Util = Boom.Util

local pairs, tonumber, tostring = pairs, tonumber, tostring
local min, tsort = math.min, table.sort

--------------------------------------------------------------------------------
-- Priority scale
--
-- GetPriority feeds two consumers: the sort order of the debuff icon row, and
-- the choice of which aura the dispel indicator points at. Both want the same
-- answer - "which of these hurts most right now" - so one scale serves both.
--------------------------------------------------------------------------------

local PRIORITY = {
	boss          = 100,  -- encounter mechanic, flagged by the client
	crowdControl  = 95,   -- the ally cannot act; usually the healer's problem next
	userImportant = 85,   -- the user asked for this one specifically
	dispellable   = 8,    -- bonus, not a floor: ranks a removable aura above a
	                      -- purely informational one of the same severity
}
AuraData.PRIORITY = PRIORITY

--------------------------------------------------------------------------------
-- Blacklist
--
-- Persistent, harmless auras that must never occupy a debuff slot. Engine/Auras
-- applies this before anything else, so a blacklisted ID is invisible even when
-- it is dispellable.
--
-- NOT blacklisted, deliberately:
--   * Resurrection Sickness (15007) - it halves the player's stats and tells the
--     healer why that person is dying. It is information, not noise.
--   * Any encounter debuff. If it is ever in doubt, it stays visible.
--
-- Extend at runtime through Boom.profile.auras.blacklist rather than editing
-- this table: profile entries survive addon updates, edits here do not.
--------------------------------------------------------------------------------

local blacklist = {
	-- Eating and drinking. Permanent while sitting in a city, meaningless in one.
	[430]    = true,  -- Drink
	[431]    = true,
	[432]    = true,
	[1133]   = true,
	[1135]   = true,
	[1137]   = true,
	[87959]  = true,
	[433]    = true,  -- Food
	[434]    = true,
	[435]    = true,
	[1127]   = true,
	[1129]   = true,
	[1131]   = true,
	[24707]  = true,
	[19705]  = true,  -- Well Fed

	-- World event / holiday flavour. Hundreds of these exist and every one of
	-- them is cosmetic; the listed few are the ones that are actually debuffs
	-- rather than buffs, so they are the only ones that could reach a frame.
	[26680]  = true,  -- Adored
	[24755]  = true,  -- Tricked or Treated
	[46668]  = true,  -- WHEE!

	-- Bloodlust fatigue. Every flavour of it, because a raid stacks several.
	[57723]  = true,  -- Exhaustion (Heroism)
	[57724]  = true,  -- Sated (Bloodlust)
	[80354]  = true,  -- Temporal Displacement (Time Warp)
	[95809]  = true,  -- Insanity (Ancient Hysteria)
	[160455] = true,  -- Fatigued (Netherwinds)
	[264689] = true,  -- Fatigued (Primal Rage)
	[390435] = true,  -- Exhaustion (Fury of the Aspects)

	-- Post-cooldown penalties. Real debuffs, but nothing a healer can act on -
	-- they describe a cooldown that already happened.
	[6788]   = true,  -- Weakened Soul
	[25771]  = true,  -- Forbearance
	[41425]  = true,  -- Hypothermia
	[11196]  = true,  -- Recently Bandaged

	-- Instance and battleground bookkeeping.
	[8326]   = true,  -- Ghost
	[26013]  = true,  -- Deserter
	[71041]  = true,  -- Dungeon Deserter
	[32727]  = true,  -- Arena Preparation
	[44521]  = true,  -- Preparation
	[206151] = true,  -- Challenger's Burden (keystone scaling, on everyone, always)
}

--------------------------------------------------------------------------------
-- Crowd control
--
-- An ally who cannot act is about to take damage they cannot avoid, so every
-- entry here outranks almost everything else on the frame.
--
-- What is NOT here, and why:
--   * Interrupts - Mind Freeze (47528), Skull Bash (106839), Rebuke (96231),
--     Disrupt (183752), Quell (351338). They land on enemies, and their silence
--     component is far too short to be worth a frame slot.
--   * Solar Beam (78675) and Garrote - Silence (1330) are silences, not loss of
--     control; a silenced ally still moves out of the fire.
--   * Roots and snares live in importantDebuffs at a low priority instead: they
--     matter, but never more than a stun does.
--------------------------------------------------------------------------------

local crowdControl = {
	-- Mage
	[118]    = true,  -- Polymorph, and every critter variant of it
	[28271]  = true,
	[28272]  = true,
	[61305]  = true,
	[61721]  = true,
	[61780]  = true,
	[126819] = true,
	[161353] = true,
	[161354] = true,
	[161355] = true,
	[161372] = true,
	[277787] = true,
	[277792] = true,
	[321395] = true,
	[391622] = true,
	[383121] = true,  -- Mass Polymorph
	[82691]  = true,  -- Ring of Frost
	[31661]  = true,  -- Dragon's Breath

	-- Hunter
	[3355]   = true,  -- Freezing Trap
	[19386]  = true,  -- Wyvern Sting
	[24394]  = true,  -- Intimidation
	[213691] = true,  -- Scatter Shot
	[117526] = true,  -- Binding Shot
	[1513]   = true,  -- Scare Beast

	-- Rogue
	[6770]   = true,  -- Sap
	[2094]   = true,  -- Blind
	[1833]   = true,  -- Cheap Shot
	[408]    = true,  -- Kidney Shot
	[1776]   = true,  -- Gouge

	-- Paladin
	[20066]  = true,  -- Repentance
	[853]    = true,  -- Hammer of Justice
	[105421] = true,  -- Blinding Light
	[10326]  = true,  -- Turn Evil

	-- Priest
	[605]    = true,  -- Mind Control
	[8122]   = true,  -- Psychic Scream
	[9484]   = true,  -- Shackle Horror (renamed from Shackle Undead)
	[88625]  = true,  -- Holy Word: Chastise
	[64044]  = true,  -- Psychic Horror

	-- Warlock
	[5782]   = true,  -- Fear (cast) ...
	[118699] = true,  -- ... and the aura it applies; both are seen in the wild
	[5484]   = true,  -- Howl of Terror
	[6789]   = true,  -- Mortal Coil
	[30283]  = true,  -- Shadowfury
	[710]    = true,  -- Banish
	[6358]   = true,  -- Seduction
	[89766]  = true,  -- Axe Toss

	-- Druid
	[33786]  = true,  -- Cyclone
	[2637]   = true,  -- Hibernate
	[5211]   = true,  -- Mighty Bash
	[99]     = true,  -- Incapacitating Roar
	[22570]  = true,  -- Maim

	-- Shaman
	[51514]  = true,  -- Hex, and every critter variant of it
	[210873] = true,
	[211004] = true,
	[211010] = true,
	[211015] = true,
	[269352] = true,
	[277778] = true,
	[277784] = true,
	[309328] = true,
	[118905] = true,  -- Capacitor Totem stun

	-- Monk
	[115078] = true,  -- Paralysis
	[119381] = true,  -- Leg Sweep

	-- Death Knight
	[47476]  = true,  -- Strangulate
	[108194] = true,  -- Asphyxiate (Frost / Unholy)
	[221562] = true,  -- Asphyxiate (Blood)
	[207167] = true,  -- Blinding Sleet

	-- Demon Hunter
	[217832] = true,  -- Imprison
	[179057] = true,  -- Chaos Nova
	[211881] = true,  -- Fel Eruption
	[207685] = true,  -- Sigil of Misery

	-- Evoker
	[360806] = true,  -- Sleep Walk
	[372245] = true,  -- Terror of the Skies

	-- Warrior
	[132169] = true,  -- Storm Bolt
	[132168] = true,  -- Shockwave
	[5246]   = true,  -- Intimidating Shout

	-- Racials
	[20549]  = true,  -- War Stomp
	[107079] = true,  -- Quaking Palm
	[255654] = true,  -- Bull Rush
	[287712] = true,  -- Haymaker
}

--------------------------------------------------------------------------------
-- Important debuffs
--
-- Mechanic classes rather than encounter lists, because encounter IDs go stale
-- within a patch and tier 1 already covers them. What survives here is what
-- stays true across every raid tier:
--
--   * anything that reduces or absorbs healing, because it changes what the
--     healer should cast next, not just how much
--   * anything that punishes a dispel, so the dispel indicator can warn instead
--     of bait
--   * the recurring Mythic+ affix debuffs, whose IDs are reused verbatim every
--     time an affix rotates back into the pool
--   * heavy physical damage over time, which no dispel will ever remove
--
-- Heal absorbs deserve a note: the amount is already drawn on the health bar by
-- the healAbsorb indicator, sourced from UnitGetTotalHealAbsorbs, so a heal
-- absorb is visible even when its spell ID is unknown to this file. The entries
-- below only add the icon and the sort order.
--------------------------------------------------------------------------------

local importantDebuffs = {
	----------------------------------------------------------- healing attention
	[209858] = 92,  -- Necrotic Wound - stacking heal absorb, the archetype
	[115804] = 90,  -- Mortal Wounds - the shared healing-reduction aura
	[8680]   = 85,  -- Wound Poison

	-- Dispelling this one silences and damages the dispeller. The dispel
	-- indicator ranks by this number, so it must outrank ordinary magic.
	[316099] = 88,  -- Unstable Affliction

	--------------------------------------------------------------- Mythic+ affix
	[240559] = 82,  -- Grievous Wound - stops only when the target is topped off
	[240443] = 80,  -- Burst
	[240447] = 70,  -- Quake

	------------------------------------------------ bleeds and physical damage
	-- Undispellable by design: the healer's only answer is to out-heal it.
	[703]    = 65,  -- Garrote
	[1943]   = 62,  -- Rupture
	[1079]   = 60,  -- Rip
	[262115] = 58,  -- Deep Wounds
	[155722] = 55,  -- Rake
	[772]    = 55,  -- Rend

	------------------------------------------------------- roots and hard snares
	-- Low priority on purpose: worth an icon when nothing worse is happening,
	-- worth nothing at all when something is.
	[339]    = 30,  -- Entangling Roots
	[122]    = 30,  -- Frost Nova
	[64695]  = 28,  -- Earthgrab
	[45524]  = 25,  -- Chains of Ice

	-------------------------------------------------------------------- lingering
	[15007]  = 20,  -- Resurrection Sickness - halved stats explain a lot of deaths
}

AuraData.blacklist        = blacklist
AuraData.crowdControl     = crowdControl
AuraData.importantDebuffs = importantDebuffs

--------------------------------------------------------------------------------
-- Queries
--
-- Called once per harmful aura per cache rebuild, so they stay branch-cheap and
-- allocation-free. Boom.profile is read live rather than cached because the
-- options panel edits it in place.
--------------------------------------------------------------------------------

local function userAuras()
	local profile = Boom.profile
	return profile and profile.auras or nil
end

function AuraData:IsBlacklisted(spellID)
	if not spellID then return false end
	if blacklist[spellID] then return true end

	local auras = userAuras()
	return (auras and auras.blacklist[spellID]) and true or false
end

function AuraData:IsCrowdControl(spellID)
	return (spellID and crowdControl[spellID]) and true or false
end

--- Should this aura earn a slot on the frame? The user's blacklist wins over
--- everything, including boss auras: an explicit "hide this" is a decision, not
--- an accident.
function AuraData:IsImportant(spellID, auraData)
	if spellID and self:IsBlacklisted(spellID) then return false end

	if auraData and auraData.isBossAura then return true end
	if not spellID then return false end

	if importantDebuffs[spellID] or crowdControl[spellID] then return true end

	local auras = userAuras()
	return (auras and auras.extraImportant[spellID]) and true or false
end

--- Higher sorts first. Never negative, so an unknown aura is simply last.
function AuraData:GetPriority(spellID, auraData)
	local priority = 0

	if spellID then
		priority = importantDebuffs[spellID] or 0

		if crowdControl[spellID] and priority < PRIORITY.crowdControl then
			priority = PRIORITY.crowdControl
		end

		local auras = userAuras()
		if auras and auras.extraImportant[spellID] and priority < PRIORITY.userImportant then
			priority = PRIORITY.userImportant
		end
	end

	if auraData then
		if auraData.isBossAura and priority < PRIORITY.boss then
			priority = PRIORITY.boss
		end

		-- Reading canDispel directly rather than through CanDispel(): this runs
		-- for every harmful aura on every tracked unit.
		local engine = Boom.Auras
		local dispelName = auraData.dispelName
		if engine and dispelName and engine.canDispel[dispelName] then
			priority = priority + PRIORITY.dispellable
		end

		-- Stacking mechanics get worse as they stack, and the whole point of a
		-- stacking debuff is that the tenth application is the dangerous one.
		local applications = auraData.applications
		if applications and applications > 1 then
			priority = priority + min(applications, 10)
		end
	end

	return priority
end

--- Which tier an aura falls into, as display text for the options list.
function AuraData:Describe(spellID, auraData)
	if spellID and self:IsBlacklisted(spellID) then return L["Hidden"] end
	if auraData and auraData.isBossAura then return L["Boss Ability"] end
	if not spellID then return L["Not Tracked"] end

	if crowdControl[spellID] then return L["Crowd Control"] end
	if importantDebuffs[spellID] then return L["Important Debuffs"] end

	local auras = userAuras()
	if auras and auras.extraImportant[spellID] then return L["Extra Important Debuffs"] end

	return L["Not Tracked"]
end

--------------------------------------------------------------------------------
-- User list editing
--
-- The options UI calls these instead of touching the profile, so the write goes
-- through Config:Set and fires BOOM_CONFIG_CHANGED like every other setting.
-- Config resolves a numeric final path segment back to a number, which is what
-- keeps "auras.extraImportant.12345" addressing the same key the aura engine
-- reads as extraImportant[12345].
--------------------------------------------------------------------------------

--- Aura verdicts are cached per unit, so an edited list has to invalidate them.
--- Heavy-handed on purpose: this runs when a human clicks a button, never in
--- combat-time code.
local function refreshCaches()
	local engine = Boom.Auras
	if engine and engine.InvalidateAll then engine:InvalidateAll() end
	Boom:MarkAllDirty(Boom.UPDATE.AURA)
end

local function setUserFlag(section, spellID, value)
	spellID = tonumber(spellID)
	if not spellID or not Boom.profile then return false, false end

	local resolved = Util.SpellExists(spellID)
	if not resolved then
		-- Still stored: the client cannot resolve every ID (removed spells, or
		-- an ID the user copied from a log), and refusing the edit would be more
		-- confusing than showing an unnamed row.
		Boom:Debug("AuraData: unresolvable spell id", tostring(spellID))
	end

	Boom.Config:Set("auras." .. section .. "." .. spellID, value)
	refreshCaches()

	return true, resolved
end

--- Returns stored, resolved - the second value lets the options panel warn that
--- the ID does not name a spell on this client.
function AuraData:AddUserImportant(spellID)
	return setUserFlag("extraImportant", spellID, true)
end

function AuraData:RemoveUserImportant(spellID)
	return setUserFlag("extraImportant", spellID, nil)
end

function AuraData:AddUserBlacklist(spellID)
	return setUserFlag("blacklist", spellID, true)
end

function AuraData:RemoveUserBlacklist(spellID)
	return setUserFlag("blacklist", spellID, nil)
end

--------------------------------------------------------------------------------
-- Diagnostics
--------------------------------------------------------------------------------

--- Curated IDs this client cannot resolve, for /boom auras. A non-empty result
--- means a spell was removed or renumbered and the entry is now dead weight -
--- harmless, but worth pruning on the next pass.
function AuraData:GetMissingSpellIDs()
	local missing = {}

	for _, set in pairs({ importantDebuffs, crowdControl, blacklist }) do
		for spellID in pairs(set) do
			if not Util.SpellExists(spellID) then
				missing[#missing + 1] = spellID
			end
		end
	end

	tsort(missing)
	return missing
end
