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

local InterruptCooldowns = { States = {}, Icons = {}, Prefix = "SUIINT1" }
SlickUI.InterruptCooldowns = InterruptCooldowns

local SPELLS = {
	[47528] = 15, [183752] = 15, [106839] = 15, [78675] = 60,
	[351338] = 40, [147362] = 24, [187707] = 15, [2139] = 24,
	[116705] = 15, [96231] = 15, [15487] = 45, [1766] = 15,
	[57994] = 12, [119910] = 24, [19647] = 24, [132409] = 24,
	[119914] = 30, [89766] = 30, [6552] = 15,
}

-- Used only to identify a member's likely interrupt icon. Inferred entries are
-- not treated as cooldown data: until a cast is observed or another SlickUI
-- client syncs, their readiness is displayed as NA.
local CLASS_INTERRUPTS = {
	DEATHKNIGHT = 47528, DEMONHUNTER = 183752, DRUID = 106839,
	EVOKER = 351338, HUNTER = 147362, MAGE = 2139,
	MONK = 116705, PALADIN = 96231, PRIEST = 15487,
	ROGUE = 1766, SHAMAN = 57994, WARLOCK = 19647, WARRIOR = 6552,
}

local function Readable(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 Settings()
	return SlickUI.DB and SlickUI.DB.interruptCooldowns
end

local function UnitNameSafe(unit)
	local name = UnitName(unit)
	return Readable(name) and type(name) == "string" and name or nil
end

local function UnitGUIDSafe(unit)
	local guid = UnitGUID(unit)
	return Readable(guid) and type(guid) == "string" and guid or nil
end

local function UnitClassColor(unit)
	local _, classToken = UnitClass(unit)
	if not Readable(classToken) or type(classToken) ~= "string" then return "ffffffff" end
	local color = RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken]
	if not color then return "ffffffff" end
	return string.format("ff%02x%02x%02x", math.floor(color.r * 255 + 0.5), math.floor(color.g * 255 + 0.5), math.floor(color.b * 255 + 0.5))
end

local function NormalizePlayerName(name)
	if not Readable(name) or type(name) ~= "string" then return nil end
	return name:lower():gsub("%s+", "")
end

local function FindUnitByName(name)
	local wanted = NormalizePlayerName(name)
	if not wanted then return nil end
	local wantedShort = wanted:match("^[^-]+")
	for index = 1, 40 do
		local unit = IsInRaid() and ("raid" .. index) or (index == 1 and "player" or "party" .. (index - 1))
		if UnitExists(unit) then
			local shortName = UnitNameSafe(unit)
			local fullName
			if UnitFullName then
				local character, realm = UnitFullName(unit)
				if Readable(character) and type(character) == "string" then
					fullName = character
					if Readable(realm) and type(realm) == "string" and realm ~= "" then
						fullName = fullName .. "-" .. realm
					end
				end
			end
			local unitShort = NormalizePlayerName(shortName)
			local unitFull = NormalizePlayerName(fullName)
			if unitFull == wanted or unitShort == wanted or unitShort == wantedShort then return unit end
		end
	end
end

function InterruptCooldowns:SetCooldown(guid, spellID, duration)
	if not Readable(guid) or type(guid) ~= "string" or type(spellID) ~= "number" then return end
	duration = tonumber(duration) or SPELLS[spellID]
	if not duration then return end
	local info = C_Spell and C_Spell.GetSpellInfo and C_Spell.GetSpellInfo(spellID)
	self.States[guid] = {
		spellID = spellID,
		icon = info and info.iconID,
		ends = GetTime() + math.max(0, duration),
	}
end

function InterruptCooldowns:SeedRosterInterrupts()
	for index = 1, 40 do
		local unit = IsInRaid() and ("raid" .. index) or (index == 1 and "player" or "party" .. (index - 1))
		if UnitExists(unit) then
			local guid = UnitGUIDSafe(unit)
			if guid and not self.States[guid] then
				local _, classToken = UnitClass(unit)
				classToken = Readable(classToken) and type(classToken) == "string" and classToken or nil
				local inferredSpell = classToken and CLASS_INTERRUPTS[classToken]
				if inferredSpell then
					self:SetCooldown(guid, inferredSpell, 0)
					if self.States[guid] then self.States[guid].inferred = true end
				end
			end
		end
	end
end

function InterruptCooldowns:UpdateFrameIcons()
	-- Interrupt readiness is shown in the dedicated floating window and
	-- information-bar widget. Keep group frames reserved for unit status and
	-- actionable cleanse indicators.
	for frame, icon in pairs(self.Icons) do
		if icon.Texture then icon.Texture:Hide() end
		if icon.Text then icon.Text:Hide() end
		self.Icons[frame] = nil
	end
end

function InterruptCooldowns:CreateWindow()
	if self.Window then return self.Window end
	local frame = CreateFrame("Frame", "SlickUIInterruptCooldownWindow", UIParent, "BackdropTemplate")
	local settings = Settings()
	frame:SetSize(settings.windowWidth or 280, settings.windowHeight or 230)
	frame:SetResizeBounds(20, 70, 650, 800)
	frame:SetResizable(true)
	frame:SetFrameStrata("MEDIUM")
	frame:SetClampedToScreen(true)
	frame.SlickMoverOnly = true
	Theme:CreatePanel(frame)
	local title = Theme:CreateText(frame, 15)
	title:SetPoint("TOPLEFT", 12, -10)
	title:SetText("Group Interrupts")
	title:SetTextColor(unpack(Theme.Colors.goldText))
	local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
	close:SetPoint("TOPRIGHT", -2, -2)
	close:SetScript("OnClick", function() InterruptCooldowns:SetWindowShown(false) end)
	local grip = CreateFrame("Button", nil, frame)
	grip:SetSize(18, 18)
	grip:SetPoint("BOTTOMRIGHT", -2, 2)
	grip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
	grip:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
	grip:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
	grip:SetScript("OnMouseDown", function(_, button) if button == "LeftButton" then frame:StartSizing("BOTTOMRIGHT") end end)
	grip:SetScript("OnMouseUp", function()
		frame:StopMovingOrSizing()
		local current = Settings()
		current.windowWidth = math.floor(frame:GetWidth() + 0.5)
		current.windowHeight = math.floor(frame:GetHeight() + 0.5)
		InterruptCooldowns:UpdateWindow()
	end)
	frame.ResizeGrip = grip
	local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
	scroll:SetPoint("TOPLEFT", 8, -32)
	scroll:SetPoint("BOTTOMRIGHT", -28, 12)
	scroll:EnableMouseWheel(true)
	local content = CreateFrame("Frame", nil, scroll)
	content:SetSize(1, 1)
	scroll:SetScrollChild(content)
	scroll:SetScript("OnMouseWheel", function(self, delta)
		local maximum = math.max(0, content:GetHeight() - self:GetHeight())
		self:SetVerticalScroll(math.max(0, math.min(maximum, self:GetVerticalScroll() - delta * 36)))
	end)
	frame.Scroll, frame.ScrollContent = scroll, content
	frame.Rows = {}
	for index = 1, 40 do
		local row = Theme:CreateText(content, 11)
		local column = math.floor((index - 1) / 20)
		local line = (index - 1) % 20
		row:SetPoint("TOPLEFT", 14 + column * 260, -35 - line * 18)
		row:SetWidth(245)
		row:SetJustifyH("LEFT")
		row:Hide()
		frame.Rows[index] = row
	end
	self.Window = frame
	SlickUI.UnitFrames.interruptCooldowns = frame
	SlickUI.Movers:Register(frame, "interruptCooldowns", "Group Interrupts")
	return frame
end

function InterruptCooldowns:UpdateWindow()
	self:SeedRosterInterrupts()
	local frame = self:CreateWindow()
	local settings = Settings()
	local alpha = math.max(0, math.min(1, tonumber(settings.backgroundAlpha) or 0.65))
	-- Theme:CreatePanel uses its own texture and edge textures, so changing only
	-- the frame backdrop does not affect the panel the player actually sees.
	if frame.SlickBackground then
		frame.SlickBackground:SetColorTexture(0.035, 0.04, 0.055, alpha)
	end
	if frame.SlickEdges then
		for _, edge in ipairs(frame.SlickEdges) do
			edge:SetAlpha(alpha)
		end
	end
	frame:SetBackdropColor(0.015, 0.018, 0.025, alpha)
	frame:SetBackdropBorderColor(Theme.Colors.border[1], Theme.Colors.border[2], Theme.Colors.border[3], alpha)
	local rowIndex = 0
	for index = 1, 40 do
		local unit = IsInRaid() and ("raid" .. index) or (index == 1 and "player" or "party" .. (index - 1))
		if UnitExists(unit) then
			local name, guid = UnitNameSafe(unit), UnitGUIDSafe(unit)
			local state = guid and self.States[guid]
			if name then
				rowIndex = rowIndex + 1
				local synchronized = state and state.inferred ~= true
				local remaining = synchronized and math.max(0, state.ends - GetTime()) or nil
				local status = not synchronized and "NA" or (remaining > 0 and (math.ceil(remaining) .. "s") or "Ready")
				local statusColor = synchronized and remaining <= 0 and "|cff55ff77" or (synchronized and "|cffffcc44" or "|cff999999")
				frame.Rows[rowIndex]:SetText(string.format("|c%s%s|r  %s%s|r", UnitClassColor(unit), name, statusColor, status))
				frame.Rows[rowIndex]:Show()
			end
		end
	end
	for index = rowIndex + 1, #frame.Rows do frame.Rows[index]:Hide() end
	local scroll, content = frame.Scroll, frame.ScrollContent
	local viewportWidth = math.max(10, scroll:GetWidth() > 0 and scroll:GetWidth() or frame:GetWidth() - 36)
	local viewportHeight = math.max(10, scroll:GetHeight() > 0 and scroll:GetHeight() or frame:GetHeight() - 44)
	local columns = viewportWidth >= 470 and 2 or 1
	local rowsPerColumn = math.max(1, math.ceil(rowIndex / columns))
	local columnWidth = math.max(10, viewportWidth / columns)
	local contentHeight = math.max(viewportHeight, rowsPerColumn * 18)
	content:SetSize(viewportWidth, contentHeight)
	for index = 1, rowIndex do
		local column = math.floor((index - 1) / rowsPerColumn)
		local line = (index - 1) % rowsPerColumn
		local row = frame.Rows[index]
		row:ClearAllPoints()
		row:SetPoint("TOPLEFT", content, "TOPLEFT", 4 + column * columnWidth, -line * 18)
		row:SetWidth(math.max(1, columnWidth - 10))
	end
	local maximumScroll = math.max(0, contentHeight - viewportHeight)
	scroll:SetVerticalScroll(math.max(0, math.min(maximumScroll, scroll:GetVerticalScroll())))
	frame:SetShown(settings.windowShown == true)
end

function InterruptCooldowns:SetWindowShown(shown)
	Settings().windowShown = shown == true
	self:UpdateWindow()
	if SlickUI.InformationBars then SlickUI.InformationBars:UpdateBlocks() end
end

function InterruptCooldowns:ToggleWindow()
	self:SetWindowShown(not Settings().windowShown)
end

function InterruptCooldowns:SetBackgroundAlpha(alpha)
	Settings().backgroundAlpha = math.max(0, math.min(1, tonumber(alpha) or 0.65))
	self:UpdateWindow()
end

function InterruptCooldowns:GetWidgetText()
	local ready, total = 0, 0
	for index = 1, 40 do
		local unit = IsInRaid() and ("raid" .. index) or (index == 1 and "player" or "party" .. (index - 1))
		local guid = UnitExists(unit) and UnitGUIDSafe(unit)
		local state = guid and self.States[guid]
		if guid then
			total = total + 1
			if state and state.inferred ~= true and state.ends <= GetTime() then ready = ready + 1 end
		end
	end
	return string.format("Interrupts: %d/%d ready", ready, total)
end

function InterruptCooldowns:OpenSettings()
	if SlickUI.Options then
		SlickUI.Options:Open()
		C_Timer.After(0, function() SlickUI.Options:SelectPage("alerts") end)
	end
end

function InterruptCooldowns:HandleWidgetClick(_, button)
	if button == "RightButton" then self:OpenSettings() else self:ToggleWindow() end
end

function InterruptCooldowns:BroadcastReadySpell()
	local spellID = SlickUI.InterruptAlerts and SlickUI.InterruptAlerts:GetSpellID()
	local guid = UnitGUIDSafe("player")
	if type(spellID) == "number" and SPELLS[spellID] and guid then
		self:SetCooldown(guid, spellID, 0)
		if C_ChatInfo and C_ChatInfo.SendAddonMessage and IsInGroup() then
			C_ChatInfo.SendAddonMessage(self.Prefix, "R;" .. tostring(spellID), IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or (IsInRaid() and "RAID" or "PARTY"))
		end
	end
end

function InterruptCooldowns:HandleUnitSpellcast(unit, castSpellID)
	if type(unit) ~= "string" or not Readable(castSpellID) or type(castSpellID) ~= "number" then return end
	local duration = SPELLS[castSpellID]
	if not duration then return end
	local guid = UnitGUIDSafe(unit)
	if not guid then return end
	self:SetCooldown(guid, castSpellID, duration)
	if unit == "player" and C_ChatInfo and C_ChatInfo.SendAddonMessage and IsInGroup() then
		C_ChatInfo.SendAddonMessage(self.Prefix, tostring(castSpellID) .. ";" .. tostring(duration), IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and "INSTANCE_CHAT" or (IsInRaid() and "RAID" or "PARTY"))
	end
end

-- Combat-log registration is forbidden to tainted addon code on current
-- clients. Group cooldowns arrive through UNIT_SPELLCAST_SUCCEEDED on the
-- interrupt-alert dispatcher and through SlickUI addon messages.
local InterruptCooldownEvents = CreateFrame("Frame")
InterruptCooldownEvents:RegisterEvent("GROUP_ROSTER_UPDATE")
InterruptCooldownEvents:RegisterEvent("PLAYER_ENTERING_WORLD")
InterruptCooldownEvents:RegisterEvent("CHAT_MSG_ADDON")

function InterruptCooldowns:Initialize()
	self:CreateWindow()
	if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then C_ChatInfo.RegisterAddonMessagePrefix(self.Prefix) end
	local events = InterruptCooldownEvents
	events:SetScript("OnEvent", function(_, event, prefix, message, _, sender)
		if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_ENTERING_WORLD" then
			InterruptCooldowns:SeedRosterInterrupts()
			C_Timer.After(1, function() InterruptCooldowns:BroadcastReadySpell() end)
		elseif event == "CHAT_MSG_ADDON" and prefix == InterruptCooldowns.Prefix and type(message) == "string" then
			local readySpell = message:match("^R;(%d+)$")
			local spellText, durationText = message:match("^(%d+);([%d%.]+)$")
			local unit = FindUnitByName(sender)
			local guid = unit and UnitGUIDSafe(unit)
			if guid and readySpell then
				InterruptCooldowns:SetCooldown(guid, tonumber(readySpell), 0)
			elseif guid then
				InterruptCooldowns:SetCooldown(guid, tonumber(spellText), tonumber(durationText))
			end
		end
	end)
	self.Events = events
	self.Ticker = C_Timer.NewTicker(0.25, function()
		InterruptCooldowns:UpdateFrameIcons()
		InterruptCooldowns:UpdateWindow()
	end)
	self:UpdateWindow()
	self:SeedRosterInterrupts()
	C_Timer.After(1, function() InterruptCooldowns:BroadcastReadySpell() end)
end

SlickUI:RegisterModule("InterruptCooldowns", InterruptCooldowns)
