local _, ns = ...
local SlickUI = ns.SlickUI

local Alerts = {
	ConditionActive = false,
	EvaluationPending = false,
	LastSoundAt = 0,
	CooldownStates = {},
	CooldownElapsed = 0,
}
SlickUI.Alerts = Alerts

local CLEANSE_SOUND = "Interface\\AddOns\\SlickUI\\Media\\Sounds\\SlickUI_DebuffCleanseReady.wav"
local ABILITY_SOUND = "Interface\\AddOns\\SlickUI\\Media\\Sounds\\SlickUI_AbilityReady.wav"

local function CanReadValue(value)
	if canaccessvalue and not canaccessvalue(value) then return false end
	if issecretvalue and issecretvalue(value) then return false end
	return true
end

local function IsKnownSpell(spellID)
	if C_SpellBook and type(C_SpellBook.IsSpellInSpellBook) == "function"
		and Enum and Enum.SpellBookSpellBank then
		local ok, known = pcall(
			C_SpellBook.IsSpellInSpellBook,
			spellID,
			Enum.SpellBookSpellBank.Player,
			true
		)
		if ok and CanReadValue(known) and known == true then return true end
	end
	if type(IsPlayerSpell) == "function" then
		local ok, known = pcall(IsPlayerSpell, spellID)
		if ok and CanReadValue(known) and known == true then return true end
	end
	if C_SpellBook and type(C_SpellBook.IsSpellKnown) == "function" then
		local ok, known = pcall(C_SpellBook.IsSpellKnown, spellID)
		if ok and CanReadValue(known) and known == true then return true end
	end
	if type(IsSpellKnown) == "function" then
		local ok, known = pcall(IsSpellKnown, spellID)
		if ok and CanReadValue(known) and known == true then return true end
	end
	return false
end

function Alerts:IsKnownSpell(spellID)
	return IsKnownSpell(spellID)
end

local function IsSpellOffCooldown(spellID)
	if not C_Spell or type(C_Spell.GetSpellCooldown) ~= "function" then return true end
	local ok, cooldown = pcall(C_Spell.GetSpellCooldown, spellID)
	if not ok or type(cooldown) ~= "table" then return true end
	if CanReadValue(cooldown.isEnabled) and cooldown.isEnabled == false then return false end
	local startTime, duration = cooldown.startTime, cooldown.duration
	-- Cooldown values can be protected during combat. Unknown must not suppress a
	-- valid HARMFUL|RAID alert; doing so made the icon appear with no sound.
	if not CanReadValue(startTime) or not CanReadValue(duration) then return true end
	if type(duration) ~= "number" or duration <= 0 then return true end
	-- A short duration is the global cooldown, not Nature's Cure's real dispel
	-- cooldown. Alert immediately so the warning is not lost when no event fires
	-- at the exact end of the global cooldown.
	if duration <= 1.75 then return true end
	if type(startTime) ~= "number" then return true end
	return startTime + duration <= GetTime()
end

local function CanCleanseType(dispelType, requireReady)
	local capabilities = SlickUI.CleanseCapabilities
	if not capabilities then return false end
	return capabilities:CanCleanse(dispelType, requireReady and IsSpellOffCooldown or nil)
end

local function HasAnyAvailableCleanse(requireReady)
	local capabilities = SlickUI.CleanseCapabilities
	if not capabilities then return false end
	return capabilities:HasAny(requireReady and IsSpellOffCooldown or nil)
end

local function UnitHasActionableDebuff(unit, requireReady)
	if not C_UnitAuras or type(C_UnitAuras.GetAuraSlots) ~= "function" then return false end
	if select(4, GetBuildInfo()) >= 120100 then return false end
	if InCombatLockdown() then return false end
	local ok, slots = pcall(function()
		return { C_UnitAuras.GetAuraSlots(unit, "HARMFUL|RAID") }
	end)
	if not ok or type(slots) ~= "table" then return false end
	for index = 2, #slots do
		-- Aura-slot tokens may be protected values in current WoW clients. Do not
		-- inspect or compare the token; pass it directly back to Blizzard's aura
		-- API, just as the working unit-frame dispel indicator does.
		local data = C_UnitAuras.GetAuraDataBySlot(unit, slots[index])
		if data then
			local _, color = SlickUI.GroupFrames:GetDispelTypeAndColor(unit, data.auraInstanceID)
			if color then
				-- Prefer the exact type when WoW exposes it. During combat it may be
				-- protected; HARMFUL|RAID plus a color result is Blizzard's safe
				-- actionable-aura signal, so fall back to any learned ready cleanse.
				if CanReadValue(data.dispelName) and type(data.dispelName) == "string" then
					if CanCleanseType(data.dispelName, requireReady) then return true end
				elseif HasAnyAvailableCleanse(requireReady) then
					return true
				end
			end
		end
	end
	return false
end

function Alerts:HasActionableGroupDebuff(requireReady)
	if UnitHasActionableDebuff("player", requireReady) then return true end
	if IsInRaid() then
		for index = 1, GetNumGroupMembers() do
			if UnitHasActionableDebuff("raid" .. index, requireReady) then return true end
		end
	elseif IsInGroup() then
		for index = 1, GetNumSubgroupMembers() do
			if UnitHasActionableDebuff("party" .. index, requireReady) then return true end
		end
	end
	return false
end

function Alerts:PlayCleanseSound()
	PlaySoundFile(CLEANSE_SOUND, "Master")
end

function Alerts:PlayAbilityReadySound()
	PlaySoundFile(ABILITY_SOUND, "Master")
end

function Alerts:GetSpellDisplay(spellID)
	if type(spellID) ~= "number" or not C_Spell then return nil, nil end
	local info = type(C_Spell.GetSpellInfo) == "function" and C_Spell.GetSpellInfo(spellID)
	if type(info) ~= "table" or not CanReadValue(info.name) then return nil, nil end
	local icon = info.iconID
	if (not CanReadValue(icon) or (type(icon) ~= "number" and type(icon) ~= "string"))
		and type(C_Spell.GetSpellTexture) == "function" then
		icon = C_Spell.GetSpellTexture(spellID)
	end
	return info.name, CanReadValue(icon) and icon or nil
end

function Alerts:CreateCooldownAlertFrame()
	if self.CooldownAlertFrame then return self.CooldownAlertFrame end
	local frame = CreateFrame("Frame", "SlickUICooldownReadyAlert", UIParent)
	frame:SetSize(220, 128)
	frame:SetPoint("CENTER", UIParent, "CENTER", 0, 130)
	frame:SetFrameStrata("HIGH")
	frame:Hide()

	local text = SlickUI.Theme:CreateText(frame, 18)
	text:SetPoint("TOP", frame, "TOP", 0, -2)
	text:SetJustifyH("CENTER")
	text:SetTextColor(unpack(SlickUI.Theme.Colors.goldText))
	frame.Label = text

	local iconFrame = CreateFrame("Frame", nil, frame)
	iconFrame:SetSize(76, 76)
	iconFrame:SetPoint("TOP", text, "BOTTOM", 0, -10)
	local background = iconFrame:CreateTexture(nil, "BACKGROUND")
	background:SetAllPoints()
	background:SetColorTexture(0.015, 0.018, 0.025, 0.96)
	local icon = iconFrame:CreateTexture(nil, "ARTWORK")
	icon:SetPoint("TOPLEFT", 4, -4)
	icon:SetPoint("BOTTOMRIGHT", -4, 4)
	icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	frame.Icon = icon
	local border = {}
	for _, values in ipairs({
		{ "TOPLEFT", "TOPRIGHT", nil, 4 },
		{ "BOTTOMLEFT", "BOTTOMRIGHT", nil, 4 },
		{ "TOPLEFT", "BOTTOMLEFT", 4, nil },
		{ "TOPRIGHT", "BOTTOMRIGHT", 4, nil },
	}) do
		local edge = iconFrame:CreateTexture(nil, "OVERLAY", nil, 6)
		edge:SetColorTexture(unpack(SlickUI.Theme.Colors.border))
		edge:SetPoint(values[1])
		edge:SetPoint(values[2])
		if values[3] then edge:SetWidth(values[3]) end
		if values[4] then edge:SetHeight(values[4]) end
		border[#border + 1] = edge
	end
	frame.Border = border

	frame:SetScript("OnUpdate", function(alert, elapsed)
		alert.Elapsed = (alert.Elapsed or 0) + elapsed
		local total = alert.Duration or 3.5
		local enter = 0.42
		local exit = 0.48
		local x, alpha, scale = 0, 1, 1
		if alert.Elapsed < enter then
			local progress = alert.Elapsed / enter
			local eased = 1 - (1 - progress) ^ 3
			x = (UIParent:GetWidth() * 0.38) * (1 - eased)
			alpha = progress
			scale = 0.68 + (0.32 * eased)
		elseif alert.Elapsed > total - exit then
			local progress = math.min(1, (alert.Elapsed - (total - exit)) / exit)
			x = -160 * progress * progress
			alpha = 1 - progress
			scale = 1 - (0.12 * progress)
		end
		alert:ClearAllPoints()
		alert:SetPoint("CENTER", UIParent, "CENTER", x, 130)
		alert:SetAlpha(alpha)
		alert:SetScale(scale)
		if alert.Elapsed >= total then
			alert:Hide()
			alert:SetAlpha(1)
			alert:SetScale(1)
		end
	end)

	self.CooldownAlertFrame = frame
	return frame
end

function Alerts:ShowCooldownReady(spellID, preview)
	local name, icon = self:GetSpellDisplay(spellID)
	if not name then return false end
	local settings = SlickUI.DB and SlickUI.DB.alerts and SlickUI.DB.alerts.cooldowns
	local frame = self:CreateCooldownAlertFrame()
	frame.Label:SetText(name .. " Ready")
	frame.Icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
	frame.Duration = settings and tonumber(settings.duration) or 3.5
	frame.Elapsed = 0
	frame:SetAlpha(0)
	frame:SetScale(0.68)
	frame:Show()
	if preview or (settings and settings.sound) then
		self:PlayAbilityReadySound()
	end
	return true
end

local function GetTrackedCooldownState(spellID, now)
	if C_Spell and type(C_Spell.GetSpellCharges) == "function" then
		local ok, charges = pcall(C_Spell.GetSpellCharges, spellID)
		if ok and type(charges) == "table"
			and CanReadValue(charges.currentCharges)
			and CanReadValue(charges.maxCharges)
			and type(charges.currentCharges) == "number"
			and type(charges.maxCharges) == "number"
			and charges.maxCharges > 1 then
			return charges.currentCharges == 0, charges.currentCharges
		end
	end
	if not C_Spell or type(C_Spell.GetSpellCooldown) ~= "function" then return nil, nil end
	local ok, cooldown = pcall(C_Spell.GetSpellCooldown, spellID)
	if not ok or type(cooldown) ~= "table" then return nil, nil end
	local startTime, duration = cooldown.startTime, cooldown.duration
	if not CanReadValue(startTime) or not CanReadValue(duration)
		or type(startTime) ~= "number" or type(duration) ~= "number" then
		return nil, nil
	end
	if duration <= 1.75 then return false, nil end
	return startTime + duration > now, nil
end

function Alerts:ResetCooldownTracking()
	wipe(self.CooldownStates)
	self.CooldownElapsed = 0
end

function Alerts:GetCurrentSpecialization()
	if type(GetSpecialization) ~= "function" or type(GetSpecializationInfo) ~= "function" then
		return nil, "Current Specialization"
	end
	local index = GetSpecialization()
	if type(index) ~= "number" then return nil, "Current Specialization" end
	local specID, name = GetSpecializationInfo(index)
	if type(specID) ~= "number" then return nil, "Current Specialization" end
	return specID, type(name) == "string" and name or ("Specialization " .. specID)
end

function Alerts:GetTrackedCooldownSpells()
	local settings = SlickUI.DB and SlickUI.DB.alerts and SlickUI.DB.alerts.cooldowns
	if not settings then return self.EmptyCooldownSpells or {} end
	if type(settings.spellsBySpec) ~= "table" then settings.spellsBySpec = {} end

	local specID = self:GetCurrentSpecialization()
	if not specID then
		self.EmptyCooldownSpells = self.EmptyCooldownSpells or {}
		return self.EmptyCooldownSpells
	end
	local key = tostring(specID)
	if type(settings.spellsBySpec[key]) ~= "table" then settings.spellsBySpec[key] = {} end

	if settings.specMigrationComplete ~= true then
		local legacy = settings.spells
		if type(legacy) == "table" and #legacy > 0 and #settings.spellsBySpec[key] == 0 then
			for _, spellID in ipairs(legacy) do
				if type(spellID) == "number" then table.insert(settings.spellsBySpec[key], spellID) end
			end
		end
		settings.spells = nil
		settings.specMigrationComplete = true
	end

	return settings.spellsBySpec[key]
end

function Alerts:EvaluateCooldowns()
	local settings = SlickUI.DB and SlickUI.DB.alerts and SlickUI.DB.alerts.cooldowns
	if not settings or not settings.enabled then return end
	local trackedSpells = self:GetTrackedCooldownSpells()
	local now = GetTime()
	local active = {}
	for _, spellID in ipairs(trackedSpells) do
		if type(spellID) == "number" and IsKnownSpell(spellID) then
			active[spellID] = true
			local onCooldown, charges = GetTrackedCooldownState(spellID, now)
			local state = self.CooldownStates[spellID]
			if onCooldown ~= nil then
				if not state then
					state = { onCooldown = onCooldown, charges = charges }
					self.CooldownStates[spellID] = state
				else
					local becameReady = state.onCooldown == true and onCooldown == false
					if type(charges) == "number" and type(state.charges) == "number"
						and charges > state.charges then
						becameReady = true
					end
					state.onCooldown = onCooldown
					state.charges = charges
					if becameReady then self:ShowCooldownReady(spellID, false) end
				end
			end
		end
	end
	for spellID in pairs(self.CooldownStates) do
		if not active[spellID] then self.CooldownStates[spellID] = nil end
	end
end

function Alerts:Evaluate()
	self.EvaluationPending = false
	local settings = SlickUI.DB and SlickUI.DB.alerts and SlickUI.DB.alerts.cleanse
	if not settings or not settings.enabled then
		self.ConditionActive = false
		return
	end

	local actionable = self:HasActionableGroupDebuff(settings.onlyWhenReady)

	if actionable and not self.ConditionActive and settings.sound then
		local now = GetTime()
		if now - self.LastSoundAt >= 1.5 then
			self.LastSoundAt = now
			self:PlayCleanseSound()
		end
	end
	self.ConditionActive = actionable
end

function Alerts:QueueEvaluation()
	if self.EvaluationPending then return end
	self.EvaluationPending = true
	C_Timer.After(0.05, function() Alerts:Evaluate() end)
end

function Alerts:Refresh()
	self.ConditionActive = false
	self:QueueEvaluation()
end

function Alerts:Initialize()
	local frame = CreateFrame("Frame")
	frame:RegisterEvent("PLAYER_ENTERING_WORLD")
	frame:RegisterEvent("GROUP_ROSTER_UPDATE")
	frame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
	frame:RegisterEvent("SPELLS_CHANGED")
	frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
	frame:RegisterEvent("UNIT_AURA")
	frame:SetScript("OnEvent", function(_, event, unit)
		if event == "PLAYER_SPECIALIZATION_CHANGED" and (unit == nil or unit == "player") then
			Alerts:ResetCooldownTracking()
			Alerts:GetTrackedCooldownSpells()
			if SlickUI.Options then
				SlickUI.Options:RefreshAlertControls()
				SlickUI.Options:RefreshCooldownSpellRows()
			end
		end
		Alerts:QueueEvaluation()
	end)
	frame:SetScript("OnUpdate", function(_, elapsed)
		Alerts.CooldownElapsed = Alerts.CooldownElapsed + elapsed
		if Alerts.CooldownElapsed >= 0.10 then
			Alerts.CooldownElapsed = 0
			Alerts:EvaluateCooldowns()
		end
	end)
	self.EventFrame = frame
	self:GetTrackedCooldownSpells()
	self:QueueEvaluation()
	self:ResetCooldownTracking()
end

SlickUI:RegisterModule("Alerts", Alerts)
