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

local PullAnnounce = {
	Names = {},
	CandidateGUID = nil,
	CandidateAt = 0,
	EncounterActive = false,
	LastGroupPullMessage = 0,
	PendingSerial = 0,
}
SlickUI.PullAnnounce = PullAnnounce

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

local function GroupUnits()
	local units = { "player" }
	if IsInRaid and IsInRaid() then
		for index = 1, GetNumGroupMembers() do units[#units + 1] = "raid" .. index end
	else
		for index = 1, math.max(0, GetNumSubgroupMembers()) do units[#units + 1] = "party" .. index end
	end
	return units
end

local function IsGroupUnit(unit)
	if unit == "player" then return true end
	if type(unit) ~= "string" then return false end
	return unit:match("^party%d+$") ~= nil or unit:match("^raid%d+$") ~= nil
end

local function InBossInstance()
	local inInstance, instanceType = IsInInstance()
	return inInstance == true and (instanceType == "party" or instanceType == "raid")
end

local function AnnouncementDelay()
	local guid = UnitGUID and UnitGUID("player")
	if not IsReadable(guid) or type(guid) ~= "string" then return 0.8 end
	local hash = 0
	for index = 1, #guid do hash = (hash * 33 + guid:byte(index)) % 20 end
	return 0.25 + hash * 0.05
end

function PullAnnounce:GetChannel()
	if IsInGroup and LE_PARTY_CATEGORY_INSTANCE and IsInGroup(LE_PARTY_CATEGORY_INSTANCE) then
		return "INSTANCE_CHAT"
	end
	if IsInRaid and IsInRaid() then return "RAID" end
	if IsInGroup and IsInGroup() then return "PARTY" end
end

function PullAnnounce:CacheRoster()
	if InCombatLockdown and InCombatLockdown() then return end
	for _, unit in ipairs(GroupUnits()) do
		local guid = UnitGUID(unit)
		local name = UnitName(unit)
		if IsReadable(guid) and IsReadable(name) and type(guid) == "string" and type(name) == "string" then
			self.Names[guid] = name
		end
	end
end

function PullAnnounce:ObserveUnit(unit)
	if self.CandidateGUID or not InBossInstance() or not IsGroupUnit(unit) then return end
	local inCombat = UnitAffectingCombat(unit)
	if not IsReadable(inCombat) or inCombat ~= true then return end
	local guid = UnitGUID(unit)
	if not IsReadable(guid) or type(guid) ~= "string" then return end
	self.CandidateGUID = guid
	self.CandidateAt = GetTime()
end

function PullAnnounce:AnyGroupCombat()
	for _, unit in ipairs(GroupUnits()) do
		local inCombat = UnitAffectingCombat(unit)
		if IsReadable(inCombat) and inCombat == true then return true end
	end
	return false
end

function PullAnnounce:AnnounceBossPull()
	local settings = SlickUI.DB and SlickUI.DB.groupControls
	if not settings or settings.pullAnnounce ~= true then return end
	local channel = self:GetChannel()
	if not channel then return end
	local name = self.CandidateGUID and self.Names[self.CandidateGUID]
	if type(name) ~= "string" or name == "" then name = "Unknown player" end

	self.PendingSerial = self.PendingSerial + 1
	local serial = self.PendingSerial
	local startedAt = GetTime()
	C_Timer.After(AnnouncementDelay(), function()
		if serial ~= PullAnnounce.PendingSerial then return end
		if PullAnnounce.LastGroupPullMessage >= startedAt then return end
		local current = SlickUI.DB and SlickUI.DB.groupControls
		if not current or current.pullAnnounce ~= true then return end
		local currentChannel = PullAnnounce:GetChannel()
		if not currentChannel then return end
		SendChatMessage("[SlickUI] First boss pull: " .. name .. ".", currentChannel)
	end)
end

function PullAnnounce:Initialize()
	local frame = CreateFrame("Frame")
	frame:RegisterEvent("GROUP_ROSTER_UPDATE")
	frame:RegisterEvent("PLAYER_ENTERING_WORLD")
	frame:RegisterEvent("PLAYER_REGEN_ENABLED")
	frame:RegisterEvent("UNIT_FLAGS")
	frame:RegisterEvent("ENCOUNTER_START")
	frame:RegisterEvent("ENCOUNTER_END")
	frame:RegisterEvent("CHAT_MSG_PARTY")
	frame:RegisterEvent("CHAT_MSG_PARTY_LEADER")
	frame:RegisterEvent("CHAT_MSG_RAID")
	frame:RegisterEvent("CHAT_MSG_RAID_LEADER")
	frame:RegisterEvent("CHAT_MSG_INSTANCE_CHAT")
	frame:RegisterEvent("CHAT_MSG_INSTANCE_CHAT_LEADER")
	frame:SetScript("OnEvent", function(_, event, arg1)
		if event == "UNIT_FLAGS" then
			PullAnnounce:ObserveUnit(arg1)
		elseif event == "ENCOUNTER_START" then
			PullAnnounce.EncounterActive = true
			if PullAnnounce.CandidateGUID and GetTime() - PullAnnounce.CandidateAt > 5 then
				PullAnnounce.CandidateGUID = nil
			end
			for _, unit in ipairs(GroupUnits()) do PullAnnounce:ObserveUnit(unit) end
			C_Timer.After(0.1, function() PullAnnounce:AnnounceBossPull() end)
		elseif event == "ENCOUNTER_END" then
			PullAnnounce.EncounterActive = false
			PullAnnounce.CandidateGUID = nil
		elseif event:match("^CHAT_MSG_") then
			if type(arg1) == "string" then
				local text = arg1:lower()
				if text:find("boss pull", 1, true) or text:find("pulled first", 1, true) then
					PullAnnounce.LastGroupPullMessage = GetTime()
				end
			end
		else
			PullAnnounce:CacheRoster()
		end
	end)
	frame:SetScript("OnUpdate", function(_, elapsed)
		PullAnnounce.UpdateElapsed = (PullAnnounce.UpdateElapsed or 0) + elapsed
		if PullAnnounce.UpdateElapsed < 0.2 then return end
		PullAnnounce.UpdateElapsed = 0
		if not PullAnnounce.EncounterActive and PullAnnounce.CandidateGUID and not PullAnnounce:AnyGroupCombat() then
			PullAnnounce.CandidateGUID = nil
		end
	end)
	self.EventFrame = frame
	self:CacheRoster()
end

SlickUI:RegisterModule("PullAnnounce", PullAnnounce)
