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

local ClassBuffs = { EvaluationPending = false, LastSignature = nil }
SlickUI.ClassBuffs = ClassBuffs

-- Durable group-wide buffs only. Learned-spell detection makes the list
-- specialization-aware without guessing which talents the player selected.
local BUFFS_BY_CLASS = {
	DRUID = { { spellID = 1126 } }, -- Mark of the Wild
	MAGE = { { spellID = 1459 } }, -- Arcane Intellect
	PRIEST = { { spellID = 21562 } }, -- Power Word: Fortitude
	WARRIOR = { { spellID = 6673 } }, -- Battle Shout
	EVOKER = { { spellID = 364342 } }, -- Blessing of the Bronze
	SHAMAN = { { spellID = 462854 } }, -- Skyfury
	PALADIN = { { spellID = 465 } }, -- Devotion Aura
}

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 SlickUI.Alerts and type(SlickUI.Alerts.IsKnownSpell) == "function" then
		return SlickUI.Alerts:IsKnownSpell(spellID)
	end
	if type(IsPlayerSpell) == "function" then
		local ok, known = pcall(IsPlayerSpell, spellID)
		return ok and CanReadValue(known) and known == true
	end
	return false
end

local function ReadBoolean(api, ...)
	if type(api) ~= "function" then return nil end
	local ok, value = pcall(api, ...)
	if not ok or not CanReadValue(value) or type(value) ~= "boolean" then return nil end
	return value
end

local function GetSpellDisplay(spellID)
	if C_Spell and type(C_Spell.GetSpellInfo) == "function" then
		local ok, info = pcall(C_Spell.GetSpellInfo, spellID)
		if ok and type(info) == "table" then return info.name, info.iconID end
	end
	if type(GetSpellInfo) == "function" then
		local ok, name, _, icon = pcall(GetSpellInfo, spellID)
		if ok then return name, icon end
	end
	return nil, nil
end

function ClassBuffs:GetAvailableBuffs()
	local _, classToken = UnitClass("player")
	local available = {}
	for _, definition in ipairs(BUFFS_BY_CLASS[classToken] or {}) do
		if IsKnownSpell(definition.spellID) then
			local name, icon = GetSpellDisplay(definition.spellID)
			if type(name) == "string" then
				available[#available + 1] = { spellID = definition.spellID, name = name, icon = icon }
			end
		end
	end
	return available
end

function ClassBuffs:IsTracked(spellID)
	local settings = SlickUI.DB and SlickUI.DB.classBuffs
	if not settings then return false end
	if type(settings.tracked) ~= "table" then settings.tracked = {} end
	return settings.tracked[tostring(spellID)] ~= false
end

function ClassBuffs:SetTracked(spellID, enabled)
	local settings = SlickUI.DB and SlickUI.DB.classBuffs
	if not settings then return end
	if type(settings.tracked) ~= "table" then settings.tracked = {} end
	settings.tracked[tostring(spellID)] = enabled == true
	self:Refresh()
end

local function UnitIsEligible(unit)
	if unit == "player" then return true end
	if ReadBoolean(UnitExists, unit) ~= true then return false end
	if ReadBoolean(UnitIsConnected, unit) ~= true then return false end
	if ReadBoolean(UnitIsDeadOrGhost, unit) ~= false then return false end
	if type(UnitInRange) ~= "function" then return false end
	local ok, inRange, checkedRange = pcall(UnitInRange, unit)
	if not ok or not CanReadValue(inRange) or not CanReadValue(checkedRange) then return false end
	return checkedRange == true and inRange == true
end

local function UnitHasBuff(unit, spellID, spellName)
	if C_UnitAuras and type(C_UnitAuras.GetAuraDataBySpellName) == "function" then
		local ok, data = pcall(C_UnitAuras.GetAuraDataBySpellName, unit, spellName, "HELPFUL")
		if ok and type(data) == "table" then return true end
	end
	if AuraUtil and type(AuraUtil.FindAuraByName) == "function" then
		local ok, aura = pcall(AuraUtil.FindAuraByName, spellName, unit, "HELPFUL")
		if ok and type(aura) == "table" then return true end
		if ok and CanReadValue(aura) and type(aura) == "string" then return true end
	end
	-- WoW 12.1 can mark even out-of-combat aura enumeration as secret. Exact
	-- name queries above are supported; retain index enumeration only for older
	-- clients where aura data remains public.
	if select(4, GetBuildInfo()) < 120100 and C_UnitAuras and type(C_UnitAuras.GetAuraDataByIndex) == "function" then
		for index = 1, 80 do
			local ok, data = pcall(C_UnitAuras.GetAuraDataByIndex, unit, index, "HELPFUL")
			if not ok or type(data) ~= "table" then break end
			if CanReadValue(data.spellId) and data.spellId == spellID then return true end
			if CanReadValue(data.name) and data.name == spellName then return true end
		end
	end
	return false
end

local function VisitNearbyGroup(callback)
	callback("player")
	if IsInRaid() then
		for index = 1, GetNumGroupMembers() do
			local unit = "raid" .. index
			if ReadBoolean(UnitIsUnit, unit, "player") == false and UnitIsEligible(unit) then callback(unit) end
		end
	elseif IsInGroup() then
		for index = 1, GetNumSubgroupMembers() do
			local unit = "party" .. index
			if UnitIsEligible(unit) then callback(unit) end
		end
	end
end

function ClassBuffs:GetMissingBuffs()
	local missing = {}
	for _, buff in ipairs(self:GetAvailableBuffs()) do
		if self:IsTracked(buff.spellID) then
			local count = 0
			VisitNearbyGroup(function(unit)
				if not UnitHasBuff(unit, buff.spellID, buff.name) then count = count + 1 end
			end)
			if count > 0 then
				missing[#missing + 1] = {
					spellID = buff.spellID, name = buff.name, icon = buff.icon, count = count,
				}
			end
		end
	end
	return missing
end

function ClassBuffs:CreateReminderFrame()
	if self.ReminderFrame then return self.ReminderFrame end
	local frame = CreateFrame("Frame", "SlickUIClassBuffReminder", UIParent, "BackdropTemplate")
	frame:SetSize(430, 58)
	frame:SetPoint("TOP", UIParent, "TOP", 0, -150)
	frame:SetFrameStrata("HIGH")
	frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
	frame:SetBackdropColor(0.03, 0.035, 0.045, 0.94)
	frame:SetBackdropBorderColor(1, 0.62, 0.08, 1)
	local icon = frame:CreateTexture(nil, "ARTWORK")
	icon:SetSize(42, 42)
	icon:SetPoint("LEFT", 8, 0)
	icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
	frame.Icon = icon
	local heading = SlickUI.Theme:CreateText(frame, 14)
	heading:SetPoint("TOPLEFT", icon, "TOPRIGHT", 12, -2)
	heading:SetPoint("RIGHT", frame, "RIGHT", -10, 0)
	heading:SetJustifyH("LEFT")
	heading:SetTextColor(unpack(SlickUI.Theme.Colors.goldText))
	frame.Heading = heading
	local detail = SlickUI.Theme:CreateText(frame, 10)
	detail:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -5)
	detail:SetPoint("RIGHT", frame, "RIGHT", -10, 0)
	detail:SetJustifyH("LEFT")
	detail:SetTextColor(unpack(SlickUI.Theme.Colors.text))
	frame.Detail = detail
	frame:Hide()
	self.ReminderFrame = frame
	return frame
end

function ClassBuffs:Evaluate()
	self.EvaluationPending = false
	local settings = SlickUI.DB and SlickUI.DB.classBuffs
	local frame = self:CreateReminderFrame()
	if not settings or settings.enabled == false then
		self.LastSignature = nil
		frame:Hide()
		return
	end
	local missing = self:GetMissingBuffs()
	if #missing == 0 then
		self.LastSignature = nil
		frame:Hide()
		return
	end
	local primary, parts, signatureParts = missing[1], {}, {}
	for _, buff in ipairs(missing) do
		parts[#parts + 1] = buff.name .. " (" .. buff.count .. ")"
		signatureParts[#signatureParts + 1] = buff.spellID .. ":" .. buff.count
	end
	frame.Icon:SetTexture(primary.icon or "Interface\\AddOns\\SlickUI\\Media\\Icons\\SlickUI_Icon.png")
	frame.Heading:SetText(#missing == 1 and ("Rebuff " .. primary.name) or "Class buffs needed")
	frame.Detail:SetText(table.concat(parts, "   "))
	frame:Show()
	self.LastSignature = table.concat(signatureParts, ",")
end

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

function ClassBuffs:Refresh()
	self.LastSignature = nil
	self:QueueEvaluation()
end

function ClassBuffs: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("UNIT_AURA")
	frame:RegisterEvent("UNIT_CONNECTION")
	frame:SetScript("OnEvent", function(_, event, unit)
		if event == "UNIT_AURA" and unit and unit ~= "player"
			and not unit:match("^party%d+$") and not unit:match("^raid%d+$") then return end
		ClassBuffs:QueueEvaluation()
		if (event == "PLAYER_SPECIALIZATION_CHANGED" or event == "SPELLS_CHANGED")
			and SlickUI.Options then SlickUI.Options:RefreshClassBuffControls() end
	end)
	self.EventFrame = frame
	self:CreateReminderFrame()
	self:QueueEvaluation()
end

SlickUI:RegisterModule("ClassBuffs", ClassBuffs)
