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

local InterruptAlerts = {}
SlickUI.InterruptAlerts = InterruptAlerts
SlickUI.Capabilities = SlickUI.Capabilities or {}
SlickUI.Capabilities.interruptAlerts = true

local INTERRUPTS = {
	DEATHKNIGHT = { 47528 }, DEMONHUNTER = { 183752 }, DRUID = { 106839, 78675 },
	EVOKER = { 351338 }, HUNTER = { 147362, 187707 }, MAGE = { 2139 },
	MONK = { 116705 }, PALADIN = { 96231 }, PRIEST = { 15487 }, ROGUE = { 1766 },
	SHAMAN = { 57994 }, WARLOCK = { 119910, 19647, 132409, 119914, 89766 }, WARRIOR = { 6552 },
}

local spellID, spellIcon
local castActive = false
local notInterruptible
local soundPlayedForCast = false
local lastAnnouncementKey, lastAnnouncementTime
local activeCastName, lastInterruptAttemptTime
local INTERRUPT_SOUND = "Interface\\AddOns\\SlickUI\\Media\\Sounds\\SlickUI_DangerBuzzer.wav"

local function IsReadable(value)
	if canaccessvalue and not canaccessvalue(value) then return false end
	if issecretvalue and issecretvalue(value) then return false end
	return value ~= nil
end

local function IsKnown(id)
	if SlickUI.Alerts and SlickUI.Alerts:IsKnownSpell(id) then return true end
	if C_SpellBook and C_SpellBook.IsSpellInSpellBook and Enum and Enum.SpellBookSpellBank then
		local ok, known = pcall(C_SpellBook.IsSpellInSpellBook, id, Enum.SpellBookSpellBank.Pet, true)
		if ok and IsReadable(known) and known == true then return true end
	end
	if GetPetActionInfo then
		for slot = 1, 10 do
			local _, _, _, _, _, _, idAtSlot = GetPetActionInfo(slot)
			if idAtSlot == id then return true end
		end
	end
	return false
end

local function IsAttackableTarget()
	local okExists, exists = pcall(UnitExists, "target")
	if not okExists or not IsReadable(exists) or exists ~= true then return false end
	local okAttack, attackable = pcall(UnitCanAttack, "player", "target")
	return okAttack and IsReadable(attackable) and attackable == true
end

function InterruptAlerts:IsEnabled()
	local settings = SlickUI.DB and SlickUI.DB.alerts and SlickUI.DB.alerts.interrupt
	return settings and settings.enabled ~= false
end

function InterruptAlerts:IsAnnouncementEnabled()
	local settings = SlickUI.DB and SlickUI.DB.alerts and SlickUI.DB.alerts.interrupt
	return settings and settings.announce == true
end

local function GetAnnouncementChannel()
	if type(IsInGroup) == "function" and LE_PARTY_CATEGORY_INSTANCE
		and IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
		return "INSTANCE_CHAT"
	end
	if type(IsInRaid) == "function" and IsInRaid() then return "RAID" end
	if type(IsInGroup) == "function" and IsInGroup() then return "PARTY" end
	return nil
end

function InterruptAlerts:AnnounceSuccessfulInterrupt(interruptedSpellName)
	if not self:IsAnnouncementEnabled() then return end
	if type(interruptedSpellName) ~= "string" or interruptedSpellName == "" then interruptedSpellName = "the cast" end
	local now = GetTime()
	local key = interruptedSpellName
	if lastAnnouncementKey == key and lastAnnouncementTime and now - lastAnnouncementTime < 0.25 then return end
	lastAnnouncementKey, lastAnnouncementTime = key, now
	local message = "Interrupted " .. interruptedSpellName .. "!"
	local channel = GetAnnouncementChannel()
	if not channel then
		SlickUI:Print(message)
		return
	end
	local sender = C_ChatInfo and C_ChatInfo.SendChatMessage or SendChatMessage
	if type(sender) == "function" then pcall(sender, message, channel) end
end

function InterruptAlerts:PlaySound(preview)
	local settings = SlickUI.DB and SlickUI.DB.alerts and SlickUI.DB.alerts.interrupt
	if preview or (settings and settings.enabled ~= false and settings.sound ~= false and IsAttackableTarget() and self:IsReady()) then
		PlaySoundFile(INTERRUPT_SOUND, "Master")
	end
end

function InterruptAlerts:IsReady()
	if not spellID or not C_Spell or not C_Spell.GetSpellCooldown then return false end
	local cooldown = C_Spell.GetSpellCooldown(spellID)
	return cooldown and cooldown.isEnabled ~= false and cooldown.isActive ~= true
end

function InterruptAlerts:CreateFrame()
	if self.Frame then return end
	local frame = CreateFrame("Frame", "SlickUIInterruptAlert", UIParent)
	frame:SetSize(92, 108)
	frame:SetPoint("CENTER", UIParent, "CENTER", 0, 45)
	frame:SetFrameStrata("HIGH")
	frame:Hide()
	local label = SlickUI.Theme:CreateText(frame, 18)
	label:SetPoint("TOP", frame, "TOP", 0, 0)
	label:SetText("Interrupt")
	label:SetTextColor(unpack(SlickUI.Theme.Colors.goldText))
	local icon = frame:CreateTexture(nil, "ARTWORK")
	icon:SetSize(76, 76)
	icon:SetPoint("TOP", label, "BOTTOM", 0, -6)
	icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	frame.Icon = icon
	self.Frame = frame
end

function InterruptAlerts:UpdateDisplay()
	local frame = self.Frame
	if not frame then return end
	if not self:IsEnabled() or not castActive or not spellIcon or not IsAttackableTarget() or not self:IsReady() then
		frame:Hide()
		return
	end
	frame.Icon:SetTexture(spellIcon)
	-- WoW 12.1 may protect interruptibility. Map it directly to alpha without
	-- reading or branching on the value in addon Lua.
	frame:SetAlphaFromBoolean(notInterruptible, 0, 1)
	frame:Show()
end

function InterruptAlerts:SetCast(active, protectedValue)
	castActive = active == true
	if not castActive then
		soundPlayedForCast = false
		activeCastName = nil
	end
	-- Assign Blizzard's protected interruptibility value without using Lua
	-- boolean operators. `and`/`or` would attempt to inspect the secret value.
	notInterruptible = protectedValue
	self:UpdateDisplay()
end

function InterruptAlerts:RefreshCast(kind)
	if not self:IsEnabled() or not spellID or not IsAttackableTarget() then return self:SetCast(false) end
	if kind == "cast" then
		local name, _, _, _, _, _, _, protected = UnitCastingInfo("target")
		activeCastName = IsReadable(name) and type(name) == "string" and name or nil
		return self:SetCast(true, protected)
	elseif kind == "channel" then
		local name, _, _, _, _, _, protected = UnitChannelInfo("target")
		activeCastName = IsReadable(name) and type(name) == "string" and name or nil
		return self:SetCast(true, protected)
	end
	local _, _, _, _, _, _, _, castProtected, _, castBarID = UnitCastingInfo("target")
	if castBarID ~= nil then return self:SetCast(true, castProtected) end
	local _, _, _, _, _, _, channelProtected, _, _, _, _, channelCastBarID = UnitChannelInfo("target")
	if channelCastBarID ~= nil then return self:SetCast(true, channelProtected) end
	self:SetCast(false)
end

function InterruptAlerts:RefreshSpell()
	spellID, spellIcon = nil, nil
	local _, class = UnitClass("player")
	for _, candidate in ipairs(INTERRUPTS[class] or {}) do
		if IsKnown(candidate) then
			spellID = candidate
			local info = C_Spell and C_Spell.GetSpellInfo and C_Spell.GetSpellInfo(candidate)
			spellIcon = info and info.iconID or (C_Spell and C_Spell.GetSpellTexture and C_Spell.GetSpellTexture(candidate))
			break
		end
	end
	self:RefreshCast()
end

function InterruptAlerts:Initialize()
	self:CreateFrame()
	local events = CreateFrame("Frame")
	events:RegisterEvent("PLAYER_TARGET_CHANGED")
	events:RegisterEvent("SPELLS_CHANGED")
	events:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
	events:RegisterEvent("TRAIT_CONFIG_UPDATED")
	events:RegisterEvent("PLAYER_TALENT_UPDATE")
	events:RegisterEvent("SPELL_UPDATE_COOLDOWN")
	events:RegisterEvent("SPELL_UPDATE_CHARGES")
	events:RegisterEvent("PLAYER_REGEN_ENABLED")
	for _, event in ipairs({ "UNIT_FACTION", "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_CHANNEL_START",
		"UNIT_SPELLCAST_CHANNEL_UPDATE", "UNIT_SPELLCAST_DELAYED", "UNIT_SPELLCAST_INTERRUPTIBLE",
		"UNIT_SPELLCAST_NOT_INTERRUPTIBLE", "UNIT_SPELLCAST_STOP", "UNIT_SPELLCAST_CHANNEL_STOP",
		"UNIT_SPELLCAST_INTERRUPTED", "UNIT_SPELLCAST_FAILED", "UNIT_SPELLCAST_FAILED_QUIET" }) do
		events:RegisterUnitEvent(event, "target")
	end
	events:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player", "pet")
	events:SetScript("OnEvent", function(_, event, unit, _, castSpellID)
		if event == "PLAYER_REGEN_ENABLED" then
			InterruptAlerts:RefreshCast()
		elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
			if IsReadable(castSpellID) and castSpellID == spellID then lastInterruptAttemptTime = GetTime() end
		elseif event == "SPELLS_CHANGED" or event == "PLAYER_SPECIALIZATION_CHANGED" or event == "TRAIT_CONFIG_UPDATED" or event == "PLAYER_TALENT_UPDATE" then
			InterruptAlerts:RefreshSpell()
		elseif event == "PLAYER_TARGET_CHANGED" or event == "UNIT_FACTION" then
			InterruptAlerts:RefreshCast()
		elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_DELAYED" then
			if event == "UNIT_SPELLCAST_START" then
				soundPlayedForCast = false
				InterruptAlerts:PlaySound(false)
				soundPlayedForCast = true
			end
			InterruptAlerts:RefreshCast("cast")
		elseif event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
			if event == "UNIT_SPELLCAST_CHANNEL_START" then
				soundPlayedForCast = false
				InterruptAlerts:PlaySound(false)
				soundPlayedForCast = true
			end
			InterruptAlerts:RefreshCast("channel")
		elseif event == "UNIT_SPELLCAST_INTERRUPTIBLE" then
			InterruptAlerts:SetCast(true, false)
			if not soundPlayedForCast then
				soundPlayedForCast = true
				InterruptAlerts:PlaySound(false)
			end
		elseif event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then
			InterruptAlerts:SetCast(true, true)
		elseif event == "UNIT_SPELLCAST_INTERRUPTED" then
			local interruptedName = activeCastName
			if lastInterruptAttemptTime and GetTime() - lastInterruptAttemptTime <= 1.25 then
				InterruptAlerts:AnnounceSuccessfulInterrupt(interruptedName)
			end
			InterruptAlerts:SetCast(false)
		elseif event == "SPELL_UPDATE_COOLDOWN" or event == "SPELL_UPDATE_CHARGES" then
			InterruptAlerts:UpdateDisplay()
		else
			InterruptAlerts:SetCast(false)
		end
	end)
	self.EventFrame = events
	self:RefreshSpell()
end

SlickUI:RegisterModule("InterruptAlerts", InterruptAlerts)
