local BeefAssist = CreateFrame("Frame", "BeefAssistEventFrame")

local inviteState = {
	pending = false,
	startIn = 0,
	waitingForRoster = false,
	min = nil,
	max = nil,
	zoneOnly = nil,
	invited = {},
	convertIn = nil,
	continueIn = nil,
}

local disbandState = {
	queue = nil,
	interval = 0.25,
	timer = 0,
	leavePending = nil,
}

local encounterState = {
	pullAnnounced = false,
	petOwners = {},
	pullScanRemaining = 0,
	pullScanTimer = 0,
	lastPullAnnounceTime = 0,
	pendingPull = nil,
}

local recentInvites = {}
local configFrame = nil
local BA_DB_VERSION = 2
local DEFAULT_INVITE_KEYWORDS = {
	["+"] = true,
	["inv"] = true,
}
local DEFAULT_AUTO_PROMOTE = {
	["beef"] = true,
	["thrall"] = true,
	["warlock"] = true,
	["jesus"] = true,
	["beefbearitto"] = true,
}
local DEFAULT_SETTINGS = {
	announceLust = false,
	announceFirstPull = false,
}
local BLOODLUST_SPELL_IDS = {
	[2825] = "Bloodlust",
	[32182] = "Heroism",
}
local PULL_EVENT_TYPES = {
	["SPELL_DAMAGE"] = true,
	["SPELL_MISSED"] = true,
	["SPELL_AURA_APPLIED"] = true,
	["SPELL_AURA_REFRESH"] = true,
	["RANGE_DAMAGE"] = true,
	["RANGE_MISSED"] = true,
	["SWING_DAMAGE"] = true,
	["SWING_MISSED"] = true,
}
local COMBATLOG_HOSTILE_MASK = COMBATLOG_OBJECT_REACTION_HOSTILE or 0x00000040
local COMBATLOG_FRIENDLY_MASK = COMBATLOG_OBJECT_REACTION_FRIENDLY or 0x00000010
local COMBATLOG_MINE_MASK = COMBATLOG_OBJECT_AFFILIATION_MINE or 0x00000001
local COMBATLOG_PARTY_MASK = COMBATLOG_OBJECT_AFFILIATION_PARTY or 0x00000002
local COMBATLOG_RAID_MASK = COMBATLOG_OBJECT_AFFILIATION_RAID or 0x00000004
local COMBATLOG_PET_MASK = COMBATLOG_OBJECT_TYPE_PET or 0x00001000
local COMBATLOG_GUARDIAN_MASK = COMBATLOG_OBJECT_TYPE_GUARDIAN or 0x00002000
local COMBATLOG_OBJECT_MASK = COMBATLOG_OBJECT_TYPE_OBJECT or 0x00004000
local PULL_ANNOUNCE_LOCKOUT = 30
local PENDING_PULL_TTL = 3
local BA_EnsureDB
local BA_FindPetOwner

local function BA_Print(msg, r, g, b)
	if ( DEFAULT_CHAT_FRAME ) then
		DEFAULT_CHAT_FRAME:AddMessage(msg, r or 1, g or 0.5, b or 0)
	end
end

local function BA_Debug(msg)
	if ( BeefAssistDB and BeefAssistDB.debug ) then
		BA_Print("<BeefAssist Debug> " .. msg, 0.6, 0.8, 1)
	end
end

CT_RA_Print = BA_Print

local function BA_Trim(s)
	if ( not s ) then
		return ""
	end
	return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end

local function BA_NormalizeName(name)
	name = BA_Trim(name or "")
	if ( name == "" ) then
		return ""
	end
	return strlower(name)
end

local function BA_AddUnique(list, value)
	local i
	for i = 1, table.getn(list) do
		if ( list[i] == value ) then
			return
		end
	end
	list[table.getn(list) + 1] = value
end

local function BA_NormalizeKeyword(keyword)
	keyword = BA_Trim(keyword or "")
	if ( keyword == "" ) then
		return ""
	end
	return strlower(keyword)
end

local function BA_GetSortedKeywordList()
	local keywords = {}
	local keywordMap = BeefAssistDB.keywords or {}
	local keyword
	for keyword in pairs(keywordMap) do
		BA_AddUnique(keywords, keyword)
	end
	table.sort(keywords)
	return keywords
end

local function BA_GetSortedPromoteList()
	local names = {}
	local autoPromote = BeefAssistDB.autoPromote or {}
	local name
	for name in pairs(autoPromote) do
		BA_AddUnique(names, name)
	end
	table.sort(names)
	return names
end

local function BA_FormatList(list)
	if ( table.getn(list) == 0 ) then
		return ""
	end
	return table.concat(list, ", ")
end

local function BA_NormalizeKeywordMap(keywordMap)
	local normalizedKeywords = {}
	local keyword
	for keyword in pairs(keywordMap or {}) do
		local normalizedKeyword = BA_NormalizeKeyword(keyword)
		if ( normalizedKeyword ~= "" ) then
			normalizedKeywords[normalizedKeyword] = true
		end
	end
	return normalizedKeywords
end

local function BA_NormalizePromoteMap(autoPromote)
	local normalizedPromote = {}
	local name
	for name in pairs(autoPromote or {}) do
		local normalizedName = BA_NormalizeName(name)
		if ( normalizedName ~= "" ) then
			normalizedPromote[normalizedName] = true
		end
	end
	return normalizedPromote
end

local function BA_SeedDefaultKeywords()
	local keyword
	for keyword in pairs(DEFAULT_INVITE_KEYWORDS) do
		BeefAssistDB.keywords[BA_NormalizeKeyword(keyword)] = true
	end
end

local function BA_SeedDefaultPromoteNames()
	local name
	for name in pairs(DEFAULT_AUTO_PROMOTE) do
		BeefAssistDB.autoPromote[name] = true
	end
end

local function BA_SeedDefaultSettings()
	local key, value
	if ( not BeefAssistDB.settings ) then
		BeefAssistDB.settings = {}
	end
	for key, value in pairs(DEFAULT_SETTINGS) do
		if ( BeefAssistDB.settings[key] == nil ) then
			BeefAssistDB.settings[key] = value
		end
	end
end

local function BA_IsSettingEnabled(key)
	if ( not BeefAssistDB ) then
		BA_EnsureDB()
	end
	return BeefAssistDB.settings and BeefAssistDB.settings[key] == true
end

local function BA_SetSetting(key, value)
	if ( not BeefAssistDB ) then
		BA_EnsureDB()
	end
	BeefAssistDB.settings[key] = value and true or false
end

local function BA_RunMigrations()
	local version = tonumber(BeefAssistDB.dbVersion) or 0

	if ( version < 1 ) then
		if ( BeefAssistDB.keyword ) then
			local migratedKeyword = BA_NormalizeKeyword(BeefAssistDB.keyword)
			if ( migratedKeyword ~= "" ) then
				BeefAssistDB.keywords[migratedKeyword] = true
			end
			BeefAssistDB.keyword = nil
		end
		BeefAssistDB.keywords = BA_NormalizeKeywordMap(BeefAssistDB.keywords)
		BeefAssistDB.autoPromote = BA_NormalizePromoteMap(BeefAssistDB.autoPromote)
		BA_SeedDefaultKeywords()
		BA_SeedDefaultPromoteNames()
		BeefAssistDB.autoPromote["befbearito"] = nil
		version = 1
	end

	if ( version < 2 ) then
		BA_SeedDefaultSettings()
		version = 2
	end

	if ( version > BA_DB_VERSION ) then
		BeefAssistDB.dbVersion = version
	else
		BeefAssistDB.dbVersion = BA_DB_VERSION
	end
end

BA_EnsureDB = function()
	if ( not BeefAssistDB ) then
		BeefAssistDB = {}
	end
	if ( not BeefAssistDB.autoPromote ) then
		BeefAssistDB.autoPromote = {}
	end
	if ( not BeefAssistDB.keywords ) then
		BeefAssistDB.keywords = {}
	end
	BA_RunMigrations()
	BA_SeedDefaultSettings()
	if ( BeefAssistDB.disbandInterval ) then
		disbandState.interval = BeefAssistDB.disbandInterval
	end
end

local function BA_GetPlayerRaidRank()
	local num = GetNumRaidMembers()
	local playerName = UnitName("player")
	for i = 1, num do
		local name, rank = GetRaidRosterInfo(i)
		if ( name == playerName ) then
			return rank or 0
		end
	end
	return 0
end

local function BA_GetRaidScanCount()
	local num = GetNumRaidMembers()
	if ( num > 25 ) then
		return 25
	end
	return num
end

local function BA_IsInRaid()
	return GetNumRaidMembers() > 0
end

local function BA_CanSendRaidWarning()
	if ( not BA_IsInRaid() ) then
		return false
	end
	return BA_GetPlayerRaidRank() >= 1
end

local function BA_IsPullAnnounceLocked()
	if ( encounterState.pullAnnounced ) then
		return true
	end
	if ( GetTime and encounterState.lastPullAnnounceTime and encounterState.lastPullAnnounceTime > 0 ) then
		return (GetTime() - encounterState.lastPullAnnounceTime) < PULL_ANNOUNCE_LOCKOUT
	end
	return false
end

local function BA_SendRaidWarning(msg)
	if ( not BA_CanSendRaidWarning() ) then
		return false
	end
	SendChatMessage(msg, "RAID_WARNING")
	return true
end

local function BA_ResetEncounterState()
	encounterState.pullAnnounced = false
	encounterState.pullScanRemaining = 0
	encounterState.pullScanTimer = 0
	encounterState.pendingPull = nil
end

local function BA_RebuildPetOwners()
	local petOwners = {}
	local playerName = UnitName("player")
	local petName

	if ( UnitExists and UnitExists("pet") ) then
		petName = UnitName("pet")
		if ( petName and playerName ) then
			petOwners[petName] = playerName
		end
	end

	if ( GetNumRaidMembers() > 0 ) then
		for i = 1, BA_GetRaidScanCount() do
			local ownerName = GetRaidRosterInfo(i)
			petName = UnitName("raidpet" .. i)
			if ( petName and ownerName ) then
				petOwners[petName] = ownerName
			end
		end
	else
		for i = 1, GetNumPartyMembers() do
			local ownerName = UnitName("party" .. i)
			petName = UnitName("partypet" .. i)
			if ( petName and ownerName ) then
				petOwners[petName] = ownerName
			end
		end
	end

	encounterState.petOwners = petOwners
end

local function BA_GetClassColorHex(className)
	local color
	if ( className and RAID_CLASS_COLORS ) then
		color = RAID_CLASS_COLORS[className]
	end
	if ( not color ) then
		return "ffffffff"
	end
	if ( color.colorStr ) then
		return color.colorStr
	end
	return string.format("ff%02x%02x%02x", math.floor((color.r or 1) * 255), math.floor((color.g or 1) * 255), math.floor((color.b or 1) * 255))
end

local function BA_ColorText(text, className)
	if ( not text or text == "" ) then
		return text
	end
	return "|c" .. BA_GetClassColorHex(className) .. text .. "|r"
end

local function BA_GetUnitClass(unit)
	if ( unit and UnitExists and UnitExists(unit) ) then
		local _, className = UnitClass(unit)
		return className
	end
	return nil
end

local function BA_GetRaidMemberClass(name)
	if ( not name or name == "" ) then
		return nil
	end
	if ( name == UnitName("player") ) then
		return BA_GetUnitClass("player")
	end
	if ( GetNumRaidMembers() > 0 ) then
		for i = 1, BA_GetRaidScanCount() do
			if ( GetRaidRosterInfo(i) == name ) then
				return BA_GetUnitClass("raid" .. i)
			end
		end
	else
		for i = 1, GetNumPartyMembers() do
			if ( UnitName("party" .. i) == name ) then
				return BA_GetUnitClass("party" .. i)
			end
		end
	end
	return nil
end

local function BA_FormatSourceName(srcName)
	if ( not srcName or srcName == "" ) then
		return "Unknown"
	end
	if ( encounterState.petOwners[srcName] ) then
		local ownerName = encounterState.petOwners[srcName]
		return BA_ColorText(ownerName .. "'s " .. srcName, BA_GetRaidMemberClass(ownerName))
	end
	return BA_ColorText(srcName, BA_GetRaidMemberClass(srcName))
end

BA_FindPetOwner = function(petName)
	if ( not petName or petName == "" ) then
		return nil
	end
	if ( encounterState.petOwners[petName] ) then
		return encounterState.petOwners[petName]
	end
	if ( UnitExists and UnitExists("pet") and UnitName("pet") == petName ) then
		local playerName = UnitName("player")
		if ( playerName ) then
			encounterState.petOwners[petName] = playerName
			return playerName
		end
	end
	if ( GetNumRaidMembers() > 0 ) then
		for i = 1, BA_GetRaidScanCount() do
			local ownerName = GetRaidRosterInfo(i)
			if ( ownerName and UnitName("raidpet" .. i) == petName ) then
				encounterState.petOwners[petName] = ownerName
				return ownerName
			end
		end
	else
		for i = 1, GetNumPartyMembers() do
			local ownerName = UnitName("party" .. i)
			if ( ownerName and UnitName("partypet" .. i) == petName ) then
				encounterState.petOwners[petName] = ownerName
				return ownerName
			end
		end
	end
	return nil
end

local function BA_IsBossUnit(unit)
	if ( not unit or not UnitExists(unit) ) then
		return false
	end
	if ( UnitIsFriend("player", unit) ) then
		return false
	end
	return UnitClassification(unit) == "worldboss" or UnitLevel(unit) == -1
end

local function BA_IsTrackedBossName(name)
	if ( not name or name == "" ) then
		return false
	end

	local function unitMatches(unit)
		return BA_IsBossUnit(unit) and UnitName(unit) == name
	end

	if ( unitMatches("target") or unitMatches("focus") ) then
		return true
	end

	if ( GetNumRaidMembers() > 0 ) then
		for i = 1, BA_GetRaidScanCount() do
			if ( unitMatches("raid" .. i .. "target") ) then
				return true
			end
		end
	else
		for i = 1, GetNumPartyMembers() do
			if ( unitMatches("party" .. i .. "target") ) then
				return true
			end
		end
	end

	return false
end

local function BA_IsFriendlyRaidSource(srcName)
	if ( not srcName or srcName == "" ) then
		return false
	end
	if ( srcName == UnitName("player") ) then
		return true
	end
	if ( GetNumRaidMembers() > 0 ) then
		for i = 1, BA_GetRaidScanCount() do
			local raidName = GetRaidRosterInfo(i)
			if ( raidName == srcName ) then
				return true
			end
		end
	end
	if ( BA_FindPetOwner(srcName) ) then
		return true
	end
	return false
end

local function BA_HasCombatLogFlag(flags, mask)
	if ( not flags or not mask or not bit or not bit.band ) then
		return false
	end
	return bit.band(flags, mask) ~= 0
end

local function BA_IsHostileCombatLogUnit(name, flags)
	if ( not name or name == "" ) then
		return false
	end
	return BA_HasCombatLogFlag(flags, COMBATLOG_HOSTILE_MASK)
end

local function BA_IsFriendlyCombatLogUnit(name)
	if ( not name or name == "" ) then
		return false
	end
	return BA_IsFriendlyRaidSource(name)
end

local function BA_IsFriendlyRaidObject(name, flags)
	if ( not name or name == "" ) then
		return false
	end
	if ( not BA_HasCombatLogFlag(flags, COMBATLOG_MINE_MASK) and not BA_HasCombatLogFlag(flags, COMBATLOG_PARTY_MASK) and not BA_HasCombatLogFlag(flags, COMBATLOG_RAID_MASK) ) then
		return false
	end
	if ( not BA_HasCombatLogFlag(flags, COMBATLOG_FRIENDLY_MASK) ) then
		return false
	end
	return BA_HasCombatLogFlag(flags, COMBATLOG_PET_MASK) or BA_HasCombatLogFlag(flags, COMBATLOG_GUARDIAN_MASK) or BA_HasCombatLogFlag(flags, COMBATLOG_OBJECT_MASK)
end

local function BA_FormatPullVictimName(name, flags)
	if ( BA_IsFriendlyRaidSource(name) ) then
		return BA_FormatSourceName(name)
	end
	if ( BA_IsFriendlyRaidObject(name, flags) ) then
		return BA_ColorText(name, "SHAMAN")
	end
	return BA_FormatSourceName(name)
end

local function BA_AnnouncePull(msg, reason)
	encounterState.pullAnnounced = true
	encounterState.pullScanRemaining = 0
	encounterState.pullScanTimer = 0
	encounterState.pendingPull = nil
	if ( GetTime ) then
		encounterState.lastPullAnnounceTime = GetTime()
	end
	BA_Debug("announcing pull (" .. reason .. "): " .. msg)
	BA_SendRaidWarning(msg)
end

local function BA_GetTime()
	if ( GetTime ) then
		return GetTime()
	end
	return 0
end

local function BA_IsPendingPullFresh()
	if ( not encounterState.pendingPull ) then
		return false
	end
	return (BA_GetTime() - encounterState.pendingPull.time) <= PENDING_PULL_TTL
end

local function BA_ClearExpiredPendingPull()
	if ( encounterState.pendingPull and not BA_IsPendingPullFresh() ) then
		BA_Debug("pending pull expired for " .. tostring(encounterState.pendingPull.bossName))
		encounterState.pendingPull = nil
	end
end

local function BA_StorePendingPull(bossName, msg, reason)
	if ( not bossName or bossName == "" or not msg or msg == "" ) then
		return
	end
	BA_ClearExpiredPendingPull()
	if ( encounterState.pendingPull ) then
		BA_Debug("pending pull kept; existing=" .. tostring(encounterState.pendingPull.bossName) .. " new=" .. tostring(bossName))
		return
	end
	encounterState.pendingPull = {
		bossName = bossName,
		msg = msg,
		reason = reason,
		time = BA_GetTime(),
	}
	encounterState.pullScanRemaining = PENDING_PULL_TTL
	encounterState.pullScanTimer = 0
	BA_Debug("pending pull stored (" .. reason .. "): " .. msg)
end

local function BA_TryAnnouncePendingPullForBoss(bossName)
	BA_ClearExpiredPendingPull()
	if ( not encounterState.pendingPull ) then
		return false
	end
	if ( encounterState.pendingPull.bossName ~= bossName ) then
		return false
	end
	BA_AnnouncePull(encounterState.pendingPull.msg, "validated " .. encounterState.pendingPull.reason)
	return true
end

local function BA_TryAnnounceVisibleBossTarget(reason)
	if ( not BA_IsInRaid() ) then
		return
	end
	if ( not BA_IsSettingEnabled("announceFirstPull") ) then
		BA_Debug(reason .. ": skipped; announceFirstPull disabled")
		return
	end
	if ( BA_IsPullAnnounceLocked() ) then
		BA_Debug(reason .. ": skipped; pull announcement locked")
		return
	end
	if ( not BA_CanSendRaidWarning() ) then
		BA_Debug(reason .. ": skipped; cannot send raid warning")
		return
	end

	local function checkBossUnit(unit)
		if ( not BA_IsBossUnit(unit) ) then
			return false
		end
		local bossName = UnitName(unit)
		local targetName = UnitName(unit .. "target")
		BA_Debug(reason .. ": visible boss " .. tostring(bossName) .. " target=" .. tostring(targetName))
		if ( BA_TryAnnouncePendingPullForBoss(bossName) ) then
			return true
		end
		if ( BA_IsFriendlyRaidSource(targetName) ) then
			BA_AnnouncePull(BA_FormatSourceName(targetName) .. " pulled " .. bossName .. ".", reason)
			return true
		end
		return false
	end

	if ( checkBossUnit("target") or checkBossUnit("focus") ) then
		return
	end

	if ( GetNumRaidMembers() > 0 ) then
		for i = 1, BA_GetRaidScanCount() do
			if ( checkBossUnit("raid" .. i .. "target") ) then
				return
			end
		end
	else
		for i = 1, GetNumPartyMembers() do
			if ( checkBossUnit("party" .. i .. "target") ) then
				return
			end
		end
	end

	BA_Debug(reason .. ": no visible boss target found")
end

local function BA_HandleLustAnnouncement(srcName, spellId)
	if ( not BA_IsInRaid() ) then
		return
	end
	if ( not BA_IsSettingEnabled("announceLust") ) then
		return
	end
	if ( not BA_CanSendRaidWarning() ) then
		return
	end
	if ( not BA_IsFriendlyRaidSource(srcName) ) then
		return
	end

	local spellName = BLOODLUST_SPELL_IDS[spellId]
	if ( not spellName ) then
		return
	end

	BA_SendRaidWarning(BA_FormatSourceName(srcName) .. " used " .. spellName .. ".")
end

local function BA_HandlePullAnnouncement(eventType, srcName, srcFlags, dstName, dstFlags)
	if ( not BA_IsInRaid() ) then
		return
	end
	if ( not BA_IsSettingEnabled("announceFirstPull") ) then
		BA_Debug("pull event skipped; announceFirstPull disabled")
		return
	end
	if ( BA_IsPullAnnounceLocked() ) then
		BA_Debug("pull event skipped; pull announcement locked")
		return
	end
	if ( not BA_CanSendRaidWarning() ) then
		BA_Debug("pull event skipped; cannot send raid warning")
		return
	end
	if ( not PULL_EVENT_TYPES[eventType] ) then
		BA_Debug("pull event skipped; eventType=" .. tostring(eventType))
		return
	end

	BA_Debug("pull event " .. tostring(eventType) .. " src=" .. tostring(srcName) .. " dst=" .. tostring(dstName) .. " srcFlags=" .. tostring(srcFlags) .. " dstFlags=" .. tostring(dstFlags))

	if ( BA_IsFriendlyRaidSource(srcName) ) then
		if ( BA_IsTrackedBossName(dstName) ) then
			BA_AnnouncePull(BA_FormatSourceName(srcName) .. " engaged " .. dstName .. ".", "friendly combat-log source")
			return
		end
		if ( BA_IsHostileCombatLogUnit(dstName, dstFlags) ) then
			BA_StorePendingPull(dstName, BA_FormatSourceName(srcName) .. " engaged " .. dstName .. ".", "friendly combat-log source")
			return
		end
		BA_Debug("pull event skipped; friendly source but destination is not tracked boss/hostile")
		return
	end

	if ( BA_IsHostileCombatLogUnit(srcName, srcFlags) and ( BA_IsFriendlyCombatLogUnit(dstName) or BA_IsFriendlyRaidObject(dstName, dstFlags) ) ) then
		if ( BA_IsTrackedBossName(srcName) ) then
			BA_AnnouncePull(BA_FormatPullVictimName(dstName, dstFlags) .. " pulled " .. srcName .. ".", "hostile combat-log source")
			return
		end
		BA_StorePendingPull(srcName, BA_FormatPullVictimName(dstName, dstFlags) .. " pulled " .. srcName .. ".", "hostile combat-log source")
		return
	end

	BA_Debug("pull event skipped; no friendly source or hostile-to-friendly inference")
end

local function BA_HandleCombatLogEvent()
	if ( not BA_IsInRaid() ) then
		return
	end
	local eventType = arg2
	local srcName = arg4
	local srcFlags = arg5
	local dstName = arg7
	local dstFlags = arg8
	local spellId = arg9

	if ( type(eventType) ~= "string" ) then
		return
	end

	if ( eventType == "SPELL_CAST_SUCCESS" and BLOODLUST_SPELL_IDS[spellId] ) then
		BA_HandleLustAnnouncement(srcName, spellId)
	end

	BA_HandlePullAnnouncement(eventType, srcName, srcFlags, dstName, dstFlags)
end

local function BA_ToggleAutoPromoteByName(name)
	if ( not name or name == "" ) then
		BA_Print("<BeefAssist> No player name provided.")
		return
	end
	name = BA_NormalizeName(name)
	BA_EnsureDB()
	if ( BeefAssistDB.autoPromote[name] ) then
		BeefAssistDB.autoPromote[name] = nil
		BA_Print("<BeefAssist> Removed |c00FFFFFF" .. name .. "|r from auto-promote list.")
	else
		BeefAssistDB.autoPromote[name] = true
		BA_Print("<BeefAssist> Added |c00FFFFFF" .. name .. "|r to auto-promote list.")
	end
end

local function BA_AutoPromoteRoster()
	if ( GetNumRaidMembers() == 0 ) then
		return
	end
	local rank = BA_GetPlayerRaidRank()
	if ( rank < 1 ) then
		return
	end
	local num = GetNumRaidMembers()
	for i = 1, num do
		local name, memberRank = GetRaidRosterInfo(i)
		if ( name and memberRank and memberRank < 1 and BeefAssistDB.autoPromote[BA_NormalizeName(name)] ) then
			PromoteToAssistant(name)
			BA_Print("<BeefAssist> Auto-Promoted |c00FFFFFF" .. name .. "|r.")
		end
	end
end

local function BA_HandleKeyword(msg)
	BA_EnsureDB()
	msg = BA_Trim(msg or "")
	local lowerMsg = strlower(msg)
	if ( msg == "" ) then
		local keywords = BA_GetSortedKeywordList()
		if ( table.getn(keywords) > 0 ) then
			BA_Print("<BeefAssist> Invite keywords: |c00FFFFFF" .. BA_FormatList(keywords) .. "|r. Use /rakeyword clear to disable.")
		else
			BA_Print("<BeefAssist> No invite keywords set.")
		end
	elseif ( lowerMsg == "off" or lowerMsg == "clear" ) then
		BeefAssistDB.keywords = {}
		BA_Print("<BeefAssist> Keyword inviting turned off.")
	else
		local normalizedKeyword = BA_NormalizeKeyword(msg)
		BeefAssistDB.keywords[normalizedKeyword] = true
		BA_Print("<BeefAssist> Added invite keyword |c00FFFFFF" .. normalizedKeyword .. "|r.")
		BA_Print("<BeefAssist> Active invite keywords: |c00FFFFFF" .. BA_FormatList(BA_GetSortedKeywordList()) .. "|r.")
	end
end

local function BA_HandleWhisper(msg, sender)
	BA_EnsureDB()
	if ( not sender ) then
		return
	end
	local cleanMsg = BA_Trim(msg or "")
	local keywordMap = BeefAssistDB.keywords or {}
	if ( BeefAssistDB.debug ) then
		BA_Print("<BeefAssist> Debug: whisper from " .. sender .. " msg='" .. cleanMsg .. "'.")
	end
	if ( not keywordMap[BA_NormalizeKeyword(cleanMsg)] ) then
		return
	end

	local canInvite = true
	local numRaid = GetNumRaidMembers()
	if ( numRaid > 0 ) then
		if ( BA_GetPlayerRaidRank() < 1 ) then
			canInvite = false
		end
	elseif ( GetNumPartyMembers() > 0 ) then
		local isLeader = false
		if ( IsPartyLeader ) then
			isLeader = IsPartyLeader()
		elseif ( UnitIsPartyLeader ) then
			isLeader = UnitIsPartyLeader("player")
		end
		if ( not isLeader ) then
			canInvite = false
		end
	end
	if ( not canInvite ) then
		BA_Print("<BeefAssist> Player '|c00FFFFFF" .. sender .. "|r' requested invite, but you don't have permission.")
		SendChatMessage("<AutoInvite> I dont have permission to invite", "WHISPER", nil, sender)
		return
	end

	local now = GetTime()
	if ( recentInvites[sender] and now - recentInvites[sender] < 2 ) then
		return
	end

	if ( numRaid >= 40 or ( numRaid == 0 and GetNumPartyMembers() >= 4 ) ) then
		BA_Print("<BeefAssist> Group is full; request from |c00FFFFFF" .. sender .. "|r.")
		SendChatMessage("The group is currently full.", "WHISPER", nil, sender)
		return
	end

	InviteUnit(sender)
	recentInvites[sender] = now
	BA_Print("<BeefAssist> Invited |c00FFFFFF" .. sender .. "|r by keyword.")
end

local function BA_ParseInviteRange(msg)
	local min, max
	local _, _, a, b = string.find(msg or "", "^(%d+)%-(%d+)$")
	if ( a and b ) then
		min = tonumber(a)
		max = tonumber(b)
	else
		local _, _, only = string.find(msg or "", "^(%d+)$")
		if ( only ) then
			min = tonumber(only)
			max = min
		end
	end
	if ( not min ) then
		return nil
	end
	if ( min < 1 ) then min = 1 end
	if ( max > 70 ) then max = 70 end
	if ( min > max ) then
		local tmp = min
		min = max
		max = tmp
	end
	return min, max
end

local function BA_InviteGuild(minLevel, maxLevel)
	local playerName = UnitName("player")
	local numInvites = 0
	local inviteBeforeRaid = 4 - GetNumPartyMembers()

	for i = 1, GetNumGuildMembers() do
		local name, _, _, level, _, _, _, _, online = GetGuildRosterInfo(i)
		if ( name and online and name ~= playerName and not inviteState.invited[i] ) then
			if ( level >= minLevel and level <= maxLevel ) then
				inviteState.invited[i] = true
				InviteUnit(name)
				numInvites = numInvites + 1
				if ( numInvites == inviteBeforeRaid and GetNumRaidMembers() == 0 ) then
					inviteState.convertIn = 1.5
					return numInvites
				end
			end
		end
	end

	return numInvites
end

local function BA_HandleInvite(msg)
	BA_EnsureDB()
	if ( not GetGuildInfo("player") ) then
		BA_Print("<BeefAssist> You need to be in a guild to mass invite.")
		return
	end

	local minLevel, maxLevel = BA_ParseInviteRange(msg)
	if ( not minLevel ) then
		BA_Print("<BeefAssist> Usage: /rainvite level or /rainvite min-max")
		return
	end

	GuildRoster()
	inviteState.pending = true
	inviteState.startIn = 0
	inviteState.waitingForRoster = true
	inviteState.min = minLevel
	inviteState.max = maxLevel
	inviteState.invited = {}
	inviteState.convertIn = nil
	inviteState.continueIn = nil
end

local function BA_HandleDisband(msg)
	BA_EnsureDB()
	local rank = BA_GetPlayerRaidRank()
	if ( rank < 1 ) then
		BA_Print("<BeefAssist> You must be raid leader or assistant to disband.")
		return
	end

	local interval = disbandState.interval or 0.5
	if ( msg and msg ~= "" ) then
		local custom = tonumber(msg)
		if ( custom ) then
			if ( custom < 0.1 ) then custom = 0.1 end
			if ( custom > 5 ) then custom = 5 end
			interval = custom
			disbandState.interval = custom
			BeefAssistDB.disbandInterval = custom
			BA_Print("<BeefAssist> Disband speed set to " .. custom .. " seconds between uninvites.")
		else
			BA_Print("<BeefAssist> Usage: /radisband [seconds between uninvites]")
		end
	end

	BA_Print("<BeefAssist> Disbanding raid...")
	SendChatMessage("<BeefAssist> Disbanding raid on request by " .. UnitName("player") .. ".", "RAID")

	local queue = {}
	for i = 1, GetNumRaidMembers() do
		local name, memberRank, _, _, _, _, _, online = GetRaidRosterInfo(i)
		if ( online and name and name ~= UnitName("player") and memberRank <= rank ) then
			queue[#queue + 1] = name
		end
	end

	if ( #queue == 0 ) then
		LeaveParty()
	else
		disbandState.queue = queue
		disbandState.timer = 0
		disbandState.leavePending = true
		disbandState.interval = interval
	end
end

local function BA_PrintHelp()
	BA_Print("<BeefAssist> Commands:")
	BA_Print("  /rakeyword <keyword> - add invite keyword")
	BA_Print("  /rakeyword clear - remove all invite keywords")
	BA_Print("  /rainvite <level|min-max> - invite guild by level")
	BA_Print("  /radisband [seconds] - disband raid (default 0.25)")
	BA_Print("  /bapromote [name] - toggle auto-promote for name or target")
	BA_Print("  /bapromotelist - list current auto-promote names")
	BA_Print("  /ba config or /ba gui - toggle config window")
	BA_Print("  /ba or /bahelp - show this help")
end

local function BA_HandlePromote(msg)
	local name = BA_Trim(msg or "")
	if ( name == "" ) then
		name = UnitName("target") or ""
	end
	if ( name == "" ) then
		BA_Print("<BeefAssist> No target or name specified.")
		return
	end
	BA_ToggleAutoPromoteByName(name)
end

local function BA_HandlePromoteList()
	BA_EnsureDB()
	local names = BA_GetSortedPromoteList()
	if ( #names == 0 ) then
		BA_Print("<BeefAssist> Auto-promote list is empty.")
	else
		BA_Print("<BeefAssist> Auto-promote list: " .. table.concat(names, ", "))
	end
end

function BeefAssist_OnFrameLoad()
	BeefAssist:SetClampedToScreen(true)
end

local function BA_SyncConfigChecks()
	if ( not configFrame ) then
		return
	end
	BeefAssistConfigAnnounceLust:SetChecked(BA_IsSettingEnabled("announceLust"))
	BeefAssistConfigAnnouncePull:SetChecked(BA_IsSettingEnabled("announceFirstPull"))
end

local function BA_CreateConfigFrame()
	configFrame = CreateFrame("Frame", "BeefAssistConfigFrame", UIParent)
	configFrame:SetWidth(320)
	configFrame:SetHeight(150)
	configFrame:SetPoint("CENTER")
	configFrame:SetFrameStrata("DIALOG")
	configFrame:SetToplevel(true)
	configFrame:EnableMouse(true)
	configFrame:SetMovable(true)
	configFrame:RegisterForDrag("LeftButton")
	configFrame:SetScript("OnDragStart", configFrame.StartMoving)
	configFrame:SetScript("OnDragStop", configFrame.StopMovingOrSizing)
	configFrame:SetBackdrop({
		bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
		edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
		tile = true,
		tileSize = 32,
		edgeSize = 32,
		insets = { left = 11, right = 12, top = 12, bottom = 11 }
	})
	configFrame:SetBackdropColor(1, 1, 1, 1)
	configFrame:Hide()

	local title = configFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	title:SetPoint("TOP", 0, -10)
	title:SetText("Beef Assist")

	local close = CreateFrame("Button", nil, configFrame, "UIPanelCloseButton")
	close:SetPoint("TOPRIGHT", -6, -6)

	local lust = CreateFrame("CheckButton", "BeefAssistConfigAnnounceLust", configFrame, "UICheckButtonTemplate")
	lust:SetPoint("TOPLEFT", 20, -36)
	lust:SetWidth(24)
	lust:SetHeight(24)
	local lustText = lust:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	lustText:SetPoint("LEFT", lust, "RIGHT", 4, 0)
	lustText:SetText("Announce Bloodlust/Heroism")
	lust:SetScript("OnClick", function(self)
		local enabled = self:GetChecked() and true or false
		BA_SetSetting("announceLust", enabled)
		BA_Print("<BeefAssist> Lust announcements " .. (enabled and "enabled." or "disabled."))
	end)

	local pull = CreateFrame("CheckButton", "BeefAssistConfigAnnouncePull", configFrame, "UICheckButtonTemplate")
	pull:SetPoint("TOPLEFT", 20, -64)
	pull:SetWidth(24)
	pull:SetHeight(24)
	local pullText = pull:CreateFontString(nil, "OVERLAY", "GameFontNormal")
	pullText:SetPoint("LEFT", pull, "RIGHT", 4, 0)
	pullText:SetText("Announce first boss pull")
	pull:SetScript("OnClick", function(self)
		local enabled = self:GetChecked() and true or false
		BA_SetSetting("announceFirstPull", enabled)
		BA_Print("<BeefAssist> First-pull announcements " .. (enabled and "enabled." or "disabled."))
	end)

	local info = configFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
	info:SetPoint("TOPLEFT", 24, -96)
	info:SetWidth(270)
	info:SetJustifyH("LEFT")
	info:SetText("Pull announcements fire once per combat when Beef Assist sees the first friendly action against a hostile target.")

	configFrame:SetScript("OnShow", BA_SyncConfigChecks)
end

local function BA_ToggleConfig()
	BA_EnsureDB()
	if ( not configFrame ) then
		BA_CreateConfigFrame()
	end
	if ( configFrame:IsShown() ) then
		configFrame:Hide()
	else
		configFrame:Show()
		BA_SyncConfigChecks()
	end
end

local function BA_HandleUICommand(msg)
	local cmd = strlower(BA_Trim(msg or ""))
	if ( cmd == "" or cmd == "help" ) then
		BA_PrintHelp()
	elseif ( cmd == "config" or cmd == "gui" ) then
		BA_ToggleConfig()
	else
		BA_PrintHelp()
	end
end

local function BA_RegisterSlashCmds()
	SlashCmdList["BEEFASSIST_KEYWORD"] = BA_HandleKeyword
	SLASH_BEEFASSIST_KEYWORD1 = "/rakeyword"
	SLASH_BEEFASSIST_KEYWORD2 = "/rakw"

	SlashCmdList["BEEFASSIST_INVITE"] = function(msg)
		BA_HandleInvite(msg)
	end
	SLASH_BEEFASSIST_INVITE1 = "/rainvite"
	SLASH_BEEFASSIST_INVITE2 = "/rainv"

	SlashCmdList["BEEFASSIST_DISBAND"] = function(msg)
		BA_HandleDisband(msg)
	end
	SLASH_BEEFASSIST_DISBAND1 = "/radisband"

	SlashCmdList["BEEFASSIST_UI"] = function(msg)
		BA_HandleUICommand(msg)
	end
	SLASH_BEEFASSIST_UI1 = "/ba"
	SLASH_BEEFASSIST_UI2 = "/batab"

	SlashCmdList["BEEFASSIST_HELP"] = function()
		BA_PrintHelp()
	end
	SLASH_BEEFASSIST_HELP1 = "/bahelp"

	SlashCmdList["BEEFASSIST_PROMOTE"] = function(msg)
		BA_HandlePromote(msg)
	end
	SLASH_BEEFASSIST_PROMOTE1 = "/bapromote"

	SlashCmdList["BEEFASSIST_PROMOTELIST"] = function()
		BA_HandlePromoteList()
	end
	SLASH_BEEFASSIST_PROMOTELIST1 = "/bapromotelist"

	SlashCmdList["BEEFASSIST_DEBUG"] = function(msg)
		BA_EnsureDB()
		msg = string.lower(BA_Trim(msg or ""))
		if ( msg == "on" ) then
			BeefAssistDB.debug = true
			BA_Print("<BeefAssist> Debug enabled.")
		elseif ( msg == "off" ) then
			BeefAssistDB.debug = nil
			BA_Print("<BeefAssist> Debug disabled.")
		else
			BA_Print("<BeefAssist> Debug is " .. (BeefAssistDB.debug and "on" or "off") .. ". Use /badebug on|off.")
		end
	end
	SLASH_BEEFASSIST_DEBUG1 = "/badebug"
end

local function BA_OnUpdate(self, elapsed)
	if ( type(elapsed) ~= "number" ) then
		elapsed = _G.arg1
	end
	if ( type(elapsed) ~= "number" ) then
		return
	end

	if ( inviteState.pending ) then
		if ( inviteState.waitingForRoster ) then
			return
		end
		inviteState.startIn = inviteState.startIn - elapsed
		if ( inviteState.startIn <= 0 ) then
			inviteState.pending = false
			local numInvites = BA_InviteGuild(inviteState.min, inviteState.max)
			if ( numInvites > 0 ) then
				if ( inviteState.min == inviteState.max ) then
					BA_Print("<BeefAssist> Invited " .. numInvites .. " guild members of level " .. inviteState.min .. ".")
				else
					BA_Print("<BeefAssist> Invited " .. numInvites .. " guild members of levels " .. inviteState.min .. " to " .. inviteState.max .. ".")
				end
			else
				BA_Print("<BeefAssist> No guild members matched the invite range.")
			end
		end
	end

	if ( inviteState.convertIn ) then
		inviteState.convertIn = inviteState.convertIn - elapsed
		if ( inviteState.convertIn <= 0 ) then
			inviteState.convertIn = nil
			ConvertToRaid()
			inviteState.continueIn = 1.5
		end
	end

	if ( inviteState.continueIn ) then
		inviteState.continueIn = inviteState.continueIn - elapsed
		if ( inviteState.continueIn <= 0 ) then
			inviteState.continueIn = nil
			BA_InviteGuild(inviteState.min, inviteState.max)
		end
	end

	if ( disbandState.queue ) then
		disbandState.timer = disbandState.timer + elapsed
		if ( disbandState.timer >= (disbandState.interval or 0.5) ) then
			disbandState.timer = 0
			local name = table.remove(disbandState.queue, 1)
			if ( name ) then
				UninviteUnit(name)
			end
			if ( #disbandState.queue == 0 ) then
				disbandState.queue = nil
				if ( disbandState.leavePending ) then
					disbandState.leavePending = nil
					LeaveParty()
				end
			end
		end
	end

	if ( BA_IsInRaid() and encounterState.pullScanRemaining and encounterState.pullScanRemaining > 0 and not encounterState.pullAnnounced ) then
		encounterState.pullScanRemaining = encounterState.pullScanRemaining - elapsed
		encounterState.pullScanTimer = encounterState.pullScanTimer - elapsed
		if ( encounterState.pullScanTimer <= 0 ) then
			encounterState.pullScanTimer = 0.25
			BA_TryAnnounceVisibleBossTarget("combat scan")
		end
		if ( encounterState.pullScanRemaining <= 0 ) then
			encounterState.pullScanRemaining = 0
			encounterState.pendingPull = nil
			BA_Debug("combat scan ended")
		end
	end
end

local function BA_OnEvent(self, event, ...)
	local ev = event
	if ( not ev and _G.event ) then
		ev = _G.event
	end
	if ( BeefAssistDB and BeefAssistDB.debug and ev ~= "COMBAT_LOG_EVENT_UNFILTERED" and ev ~= "UNIT_TARGET" ) then
		BA_Print("<BeefAssist> Debug: event " .. tostring(ev))
	end
	if ( ev == "ADDON_LOADED" ) then
		local name = ...
		if ( name == "Beef-Assist" ) then
			BA_EnsureDB()
			BA_RebuildPetOwners()
			BA_ResetEncounterState()
			BeefAssist:SetScript("OnUpdate", BA_OnUpdate)
		end
	elseif ( ev == "CHAT_MSG_WHISPER" ) then
		local msg, sender = ...
		if ( not msg ) then msg = arg1 end
		if ( not sender ) then sender = arg2 end
		BA_HandleWhisper(msg, sender)
	elseif ( ev == "RAID_ROSTER_UPDATE" ) then
		if ( BA_IsInRaid() ) then
			BA_RebuildPetOwners()
		else
			encounterState.petOwners = {}
		end
		BA_AutoPromoteRoster()
	elseif ( ev == "UNIT_PET" ) then
		if ( BA_IsInRaid() ) then
			BA_RebuildPetOwners()
		end
	elseif ( ev == "COMBAT_LOG_EVENT_UNFILTERED" ) then
		BA_HandleCombatLogEvent()
	elseif ( ev == "PLAYER_REGEN_DISABLED" ) then
		if ( BA_IsInRaid() and not BA_IsPullAnnounceLocked() ) then
			encounterState.pullScanRemaining = 3
			encounterState.pullScanTimer = 0
		end
		BA_Debug("PLAYER_REGEN_DISABLED")
		BA_TryAnnounceVisibleBossTarget("combat start")
	elseif ( ev == "PLAYER_REGEN_ENABLED" ) then
		BA_Debug("PLAYER_REGEN_ENABLED")
		BA_ResetEncounterState()
	elseif ( ev == "PLAYER_ENTERING_WORLD" ) then
		if ( BA_IsInRaid() ) then
			BA_RebuildPetOwners()
		end
		BA_ResetEncounterState()
	elseif ( ev == "UNIT_TARGET" ) then
		if ( BA_IsInRaid() and UnitAffectingCombat and UnitAffectingCombat("player") ) then
			BA_TryAnnounceVisibleBossTarget("unit target")
		end
	elseif ( ev == "GUILD_ROSTER_UPDATE" ) then
		if ( inviteState.pending and inviteState.waitingForRoster ) then
			inviteState.waitingForRoster = false
		end
	end
end

BA_RegisterSlashCmds()
BeefAssist:RegisterEvent("ADDON_LOADED")
BeefAssist:RegisterEvent("CHAT_MSG_WHISPER")
BeefAssist:RegisterEvent("RAID_ROSTER_UPDATE")
BeefAssist:RegisterEvent("UNIT_PET")
BeefAssist:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
BeefAssist:RegisterEvent("PLAYER_REGEN_DISABLED")
BeefAssist:RegisterEvent("PLAYER_REGEN_ENABLED")
BeefAssist:RegisterEvent("PLAYER_ENTERING_WORLD")
BeefAssist:RegisterEvent("UNIT_TARGET")
BeefAssist:RegisterEvent("GUILD_ROSTER_UPDATE")
BeefAssist:SetScript("OnEvent", BA_OnEvent)

