--[[
	Cerebro - EPGP Loot System for RCLootCouncil
	Core.lua - Main addon initialization and event handling
]]--

-- Create addon using Ace3
local AddonName = "Cerebro"
Cerebro = LibStub("AceAddon-3.0"):NewAddon(AddonName, "AceEvent-3.0", "AceConsole-3.0", "AceComm-3.0", "AceSerializer-3.0", "AceTimer-3.0")

-- Addon version
Cerebro.version = "0.8.14"
Cerebro.addonName = AddonName

-- Quick reference to modules (populated on module load)
Cerebro.modules = {}

--[[=============================================
	Initialization
===============================================]]

function Cerebro:OnInitialize()
	-- Initialize database
	self:InitDatabase()

	-- Register slash commands
	self:RegisterChatCommand("cerebro", "SlashCommand")
	self:RegisterChatCommand("cb", "SlashCommand")

	-- Register addon communication channel
	self:RegisterComm("Cerebro", "OnCommReceived")

	-- Setup configuration options
	self:SetupOptions()

	self:Print("Initialized. Type /cerebro or /cb for commands.")

	-- Popup for Decay
	StaticPopupDialogs["CEREBRO_CONFIRM_DECAY"] = {
		text = "Are you sure you want to run Weekly Decay? This will reduce all EP/GP by the configured percentage.",
		button1 = "Yes",
		button2 = "No",
		OnAccept = function()
			local epgp = Cerebro:GetModule("EPGP")
			local count, epDecayed, gpDecayed = epgp:Decay()
			Cerebro.Ledger:AddAuditLog("DECAY_RUN", string.format("Processed %d players. -%d EP, -%d GP", count, epDecayed, gpDecayed))
			Cerebro:Print(string.format("Decay complete. Processed %d players. Removed %d EP and %d GP total.", 
				count, epDecayed, gpDecayed))
			Cerebro:GetModule("UI"):RefreshStandings()
		end,
		timeout = 0,
		whileDead = true,
		hideOnEscape = true,
		preferredIndex = 3,
	}

	-- Popup for Guild Scope Lock Confirmation (Bootstrapping)
	StaticPopupDialogs["CEREBRO_CONFIRM_SCOPE_LOCK"] = {
		text = "Lock Cerebro to guild: |cff00ff00%s|r?\n\nThis cannot be undone without wiping all data.\nOnly characters in this guild will have access.",
		button1 = "Lock to This Guild",
		button2 = "Cancel",
		OnAccept = function()
			if Cerebro:SetAuthorizedGuild() then
				local scope = Cerebro:GetAuthorizedGuild()
				Cerebro:Print("Guild scope locked to: " .. (scope and scope.guildName or "Unknown"))
				-- Retry UI show
				if Cerebro.modules.UI then
					Cerebro.modules.UI:Show()
				end
			else
				Cerebro:Print("Failed to lock guild scope. Guild info may not be ready.")
			end
		end,
		timeout = 0,
		whileDead = true,
		hideOnEscape = true,
		preferredIndex = 3,
	}

	-- Popup for Guild Scope Override (Wipe and Switch)
	-- Constants for backup size limits
	local BACKUP_MAX_TRANSACTIONS = 500
	local BACKUP_MAX_AUDIT = 200
	
	-- Helper: Get last N entries from a list
	local function TrimList(list, maxEntries)
		if not list or #list <= maxEntries then
			return Cerebro:CopyTable(list or {})
		end
		local trimmed = {}
		local start = #list - maxEntries + 1
		for i = start, #list do
			table.insert(trimmed, Cerebro:CopyTable(list[i]))
		end
		return trimmed
	end
	
	StaticPopupDialogs["CEREBRO_CONFIRM_SCOPE_OVERRIDE"] = {
		text = "WARNING: Switching guilds will PERMANENTLY DELETE all EPGP data.\n\nThis includes: Players, Transactions, Audit Logs, Awards.\n\nType WIPE to confirm:",
		button1 = "Wipe and Switch",
		button2 = "Cancel",
		hasEditBox = true,
		OnAccept = function(self)
			-- Wrap in pcall to catch any silent errors
			local success, err = pcall(function()
				local confirmText = strtrim(self.editBox:GetText() or ""):upper()
				if confirmText ~= "WIPE" then
					Cerebro:Print("Confirmation failed. Type WIPE exactly.")
					return
				end
				
				-- Create backup before wipe (with size limits)
				local backup = {
					timestamp = time(),
					version = Cerebro.version,
					players = Cerebro:CopyTable(Cerebro.db.global.players),
					transactions = TrimList(Cerebro.db.global.transactions, BACKUP_MAX_TRANSACTIONS),
					audit = TrimList(Cerebro.db.global.audit, BACKUP_MAX_AUDIT),
					scope = Cerebro:CopyTable(Cerebro.db.global.scope),
					reason = "SCOPE_OVERRIDE",
					trimmed = {
						transactions = #(Cerebro.db.global.transactions or {}) > BACKUP_MAX_TRANSACTIONS,
						audit = #(Cerebro.db.global.audit or {}) > BACKUP_MAX_AUDIT,
					},
				}
				Cerebro.db.global.backup = backup
				
				-- Log before wipe
				if Cerebro.Ledger and Cerebro.Ledger.AddAuditLog then
					Cerebro.Ledger:AddAuditLog("BACKUP_CREATED", "Before scope override")
				end
				
				-- Wipe data
				Cerebro.db.global.players = {}
				Cerebro.db.global.transactions = {}
				Cerebro.db.global.audit = {}
				Cerebro.db.global.awards = {}
				Cerebro.db.global.currentSession = nil
				Cerebro.db.global.scope = nil  -- Clear scope for re-locking
				
				-- Activate quarantine to prevent sync from re-injecting stale scope
				-- This is critical: without quarantine, an incoming officer sync 
				-- will immediately re-inject the old scope before user can type /cb
				Cerebro.db.global.resetQuarantine = {
					at = time(),
					reason = "SCOPE_OVERRIDE"
				}
				
				-- Log after wipe (to fresh audit table)
				if Cerebro.Ledger and Cerebro.Ledger.AddAuditLog then
					Cerebro.Ledger:AddAuditLog("SCOPE_OVERRIDE", "Data wiped for guild switch")
				end
				
				Cerebro:Print("Data wiped. Open /cb to lock to new guild.")
				
				-- Close UI
				if Cerebro.modules.UI then
					Cerebro.modules.UI:Hide()
				end
			end)
			
			if not success then
				Cerebro:Print("|cffff0000ERROR during scope override:|r " .. tostring(err))
			end
		end,
		OnShow = function(self)
			self.editBox:SetText("")
		end,
		timeout = 0,
		whileDead = true,
		hideOnEscape = true,
		preferredIndex = 3,
	}

	-- Clean up corrupted "player" entries from DB
	if self.db.global.players["player"] then
		self.db.global.players["player"] = nil
		self:Print("Removed invalid 'player' entry from database.")
	end
end

function Cerebro:SetupOptions()
	--[[
		SCHEMA-DRIVEN OPTIONS GENERATOR
		
		This function generates the entire Blizzard Interface Options UI
		dynamically from UI/ConfigSchema.lua. No hardcoded option tables.
		
		The schema is the SINGLE source of truth for all settings.
	]]--
	
	if not _G.UI_ConfigSchema then
		self:Print("ERROR: ConfigSchema not loaded. Cannot setup options.")
		return
	end
	
	local schema = _G.UI_ConfigSchema
	
	-- Build base options table
	local options = {
		type = "group",
		name = "Cerebro",
		args = {},
	}
	
	-- Generate "Commands" category (static, not from schema)
	options.args.commands = {
		type = "group",
		name = "Commands",
		order = 0,
		args = {
			header_gen = { type = "header", name = "General", order = 1 },
			show_ui = {
				type = "execute",
				name = "Show Main UI",
				desc = "Toggle the main window (/cb show)",
				func = function() self.modules.UI:Toggle() end,
				order = 2,
			},
			wishlist = {
				type = "execute",
				name = "Wishlist",
				desc = "Manage Wishlist (/cb wishlist)",
				func = function() self.modules.UI:ShowWishlist() end,
				order = 4,
			},
			standings = {
				type = "execute",
				name = "Standings",
				desc = "View EPGP Standings (/cb standings)",
				func = function() self.modules.UI:ShowStandings() end,
				order = 5,
			},
			history = {
				type = "execute",
				name = "History",
				desc = "View Loot History (/cb history)",
				func = function() self.modules.UI:ShowHistory() end,
				order = 6,
			},
			
			header_discord = { 
				type = "header", 
				name = "Discord", 
				order = 10,
				hidden = function() return not self.modules.DiscordExport end 
			},
			discord = {
				type = "execute",
				name = "Discord Export",
				desc = "Show JSON for Discord Webhook (/cb discord)",
				func = function() 
					if self.modules.DiscordExport then 
						self.modules.DiscordExport:ShowExportWindow() 
					end
				end,
				hidden = function() return not self.modules.DiscordExport end,
				order = 11,
			},

			header_off = { type = "header", name = "Officer Tools", order = 20 },
			start_session = {
				type = "execute",
				name = "Start Session",
				desc = "Start a new raid session (/cb start)",
				func = function() self.modules.Session:StartSession() end,
				disabled = function() return not self:IsOfficerSafe() end,
				order = 21,
			},
			end_session = {
				type = "execute",
				name = "End Session",
				desc = "End current session (/cb end)",
				func = function() self.modules.Session:EndSession() end,
				disabled = function() return not self:IsOfficerSafe() end,
				order = 22,
			},
			prep_check = {
				type = "execute",
				name = "Prep Check",
				desc = "Run Preparation Check (/cb check)",
				func = function() self.modules.Preparation:CheckRaid() end,
				disabled = function() return not self:IsOfficerSafe() end,
				order = 23,
			},
			sync = {
				type = "execute",
				name = "Sync to Raid",
				desc = "Force data sync (/cb sync)",
				func = function() self.modules.Communication:SyncToRaid() end,
				disabled = function() return not self:IsOfficerSafe() end,
				order = 24,
			},
		},
	}
	
	-- Generate categories from schema (deterministically sorted)
	local categories = schema:GetCategoriesSorted()
	local categoryOrder = 1
	
	for _, categoryName in ipairs(categories) do
		-- Create category group
		local categoryKey = categoryName:gsub("[^%w]", "_"):lower()
		
		options.args[categoryKey] = {
			type = "group",
			name = categoryName,
			order = categoryOrder,
			args = {},
		}
		
		-- Group items by group field
		local groups = {}
		local groupOrder = {}
		
		for _, item in ipairs(schema:GetItemsForCategory(categoryName)) do
			local groupName = item.group or "General"
			if not groups[groupName] then
				groups[groupName] = {}
				table.insert(groupOrder, groupName)
			end
			table.insert(groups[groupName], item)
		end
		
		-- Generate options for each group
		local currentOrder = 1
		for _, groupName in ipairs(groupOrder) do
			-- Add header for group
			if groupName ~= "General" or #groupOrder > 1 then
				local headerKey = "header_" .. groupName:gsub("[^%w]", "_"):lower()
				options.args[categoryKey].args[headerKey] = {
					type = "header",
					name = groupName,
					order = currentOrder,
				}
				currentOrder = currentOrder + 1
			end
			
			-- Add items in group
			for _, item in ipairs(groups[groupName]) do
				local optionDef = self:GenerateOption(item)
				if optionDef then
					options.args[categoryKey].args[item.id] = optionDef
					optionDef.order = currentOrder
					currentOrder = currentOrder + 1
				end
			end
		end
		
		categoryOrder = categoryOrder + 1
	end
	
	-- Register with AceConfig
	LibStub("AceConfig-3.0"):RegisterOptionsTable("Cerebro", options)
	
	-- Add to Blizzard options
	self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("Cerebro", "Cerebro")
end

-- Generate a single AceConfig option from a schema item
function Cerebro:GenerateOption(item)
	local option = {
		name = item.label,
		desc = item.desc or "",
	}
	
	-- Type mapping
	if item.type == "bool" then
		option.type = "toggle"
	elseif item.type == "number" then
		if item.min and item.max then
			option.type = "range"
			option.min = item.min
			option.max = item.max
			option.step = item.step or 1
			option.isPercent = item.isPercent
		else
			option.type = "input"
		end
	elseif item.type == "string" then
		option.type = "input"
	elseif item.type == "select" then
		option.type = "select"
		option.values = item.choices or {}
	else
		return nil -- Unknown type
	end
	
	-- Getter
	option.get = function(info)
		local value = _G.UI_ConfigSchema:GetValue(item)
		
		-- For input fields (non-range numbers/strings), convert to string
		if option.type == "input" and item.type == "number" then
			return tostring(value)
		end
		
		return value
	end
	
	-- Setter
	option.set = function(info, val)
		_G.UI_ConfigSchema:SetValue(item, val)
	end
	
	-- RBAC gating
	if item.officerOnly then
		local mode = item.officerOnlyMode or "hidden"
		if mode == "hidden" then
			option.hidden = function() return not self:IsOfficerSafe() end
		elseif mode == "disabled" then
			option.disabled = function() return not self:IsOfficerSafe() end
		end
	end
	
	return option
end


function Cerebro:OnEnable()
	-- Register events
	self:RegisterEvent("ENCOUNTER_START")
	self:RegisterEvent("ENCOUNTER_END")
	-- self:RegisterEvent("CHAT_MSG_LOOT") -- RCLC Bridge handles this via RCLC API now? Or we should look for auto-pass
	self:RegisterEvent("GROUP_ROSTER_UPDATE")
	self:RegisterEvent("PLAYER_ENTERING_WORLD")

	-- Start periodic tasks
	self:ScheduleRepeatingTimer("OnHeartbeat", 30) -- Every 30 seconds

	self:Print("Enabled. Version " .. self.version)

	-- Check if we're in a raid
	if IsInRaid() then
		self:CheckRaidStatus()
	end
	

end

function Cerebro:OnDisable()
	self:Print("Disabled.")
end

--[[=============================================
	Event Handlers
===============================================]]

function Cerebro:ENCOUNTER_START(event, encounterID, encounterName, difficultyID, groupSize)
	self:Debug("Encounter started: " .. encounterName .. " (ID: " .. encounterID .. ")")

	-- Lock bidding if enabled
	if self.modules.Session and self.modules.Session:IsActive() then
		self:SendMessage("ENCOUNTER_START", encounterID, encounterName)
	end
end

function Cerebro:ENCOUNTER_END(event, encounterID, encounterName, difficultyID, groupSize, success)
	self:Debug("Encounter ended: " .. encounterName .. " (Success: " .. tostring(success) .. ")")

	if success and self.modules.Session and self.modules.Session:IsActive() then
		-- FIX: Only officers process boss kills to prevent non-officers from awarding EP
		-- Non-officers may have currentSession synced from an officer, but should not process kills
		if self:IsOfficer() then
			self.modules.Session:RecordBossKill(encounterID, encounterName, difficultyID)
			self:SendMessage("BOSS_KILL", encounterID, encounterName)
		end
	end
end

function Cerebro:BOSS_KILL(event, ...)
	-- Alternative boss kill detection
	self:Debug("BOSS_KILL event fired")
end

function Cerebro:CHAT_MSG_LOOT(event, text, playerName, ...)
	-- RCLC handles loot, so we don't need to do much here unless we want to log it
end

function Cerebro:GROUP_ROSTER_UPDATE(event)
	if IsInRaid() then
		-- Sync on raid join (non-officers only, once per raid)
		if not self:IsOfficer() and not self._hasRaidSynced then
			-- Guard: skip if quarantined
			if self.modules.Communication and self.modules.Communication:IsQuarantined() then
				self:Debug("GROUP_ROSTER_UPDATE sync skipped (quarantine)")
			else
				self._hasRaidSynced = true
				self:ScheduleTimer(function()
					self.modules.Communication:RequestSync()
				end, 2)
			end
		end
		-- Update raid roster
		if self.modules.Session and self.modules.Session:IsActive() then
			self.modules.Session:UpdateRoster()
		end
	else
		self._hasRaidSynced = nil  -- Reset when leaving raid
		-- Left raid - end session
		if self.modules.Session and self.modules.Session:IsActive() then
			self.modules.Session:EndSession()
		end
	end
end

function Cerebro:PLAYER_ENTERING_WORLD(event, isInitialLogin, isReloadingUi)
	if isInitialLogin or isReloadingUi then
		self:CheckRaidStatus()

		-- Request sync from officers if we're not one
		if not self:IsOfficer() then
			-- Guard: skip if quarantined
			if self.modules.Communication and self.modules.Communication:IsQuarantined() then
				self:Debug("PLAYER_ENTERING_WORLD sync skipped (quarantine)")
			else
				self:ScheduleTimer(function()
					self.modules.Communication:RequestSync()
				end, 5)
			end
		end
		
		-- Check for season changes (delay to ensure API is ready)
		self:ScheduleTimer(function()
			self:CheckSeasonChange()
		end, 10)
	end
end

--[[=============================================
	Periodic Tasks
===============================================]]

function Cerebro:OnHeartbeat()
	-- Sync data if we're an officer
	if self:IsOfficer() and IsInRaid() then
		self.modules.Communication:SyncToRaid()
	end
end

--[[=============================================
	Utility Functions
===============================================]]

function Cerebro:CheckRaidStatus()
	if IsInRaid() then
		local numMembers = GetNumGroupMembers()
		self:Debug("In raid with " .. numMembers .. " members")

		-- Auto-start session if we're an officer
		if self:IsOfficer() and not self.modules.Session:IsActive() then
			self:Print("You're in a raid. Type /pb start to begin a session.")
		end
	end
end

-- TTL-based Command Execution Guard (prevent double-execution)
Cerebro.CommandExecutionGuard = {
	recent = {}, -- { [hash] = timestamp }
	duplicateWindow = 1.0, -- seconds
}

function Cerebro.CommandExecutionGuard:GenerateHash(cmdId, argv)
	-- Normalize args to stable string
	local argStr = table.concat(argv or {}, "|")
	return cmdId .. "||" .. argStr
end

function Cerebro.CommandExecutionGuard:IsDuplicate(cmdId, argv)
	local hash = self:GenerateHash(cmdId, argv)
	local now = GetTime()
	
	-- Prune old entries (older than 5 seconds)
	for h, ts in pairs(self.recent) do
		if now - ts > 5.0 then
			self.recent[h] = nil
		end
	end
	
	-- Check if recent
	if self.recent[hash] and (now - self.recent[hash] < self.duplicateWindow) then
		return true
	end
	
	-- Record this execution
	self.recent[hash] = now
	return false
end

-- Unified Command Executor (Idempotency + RBAC + Registry dispatch)
function Cerebro:ExecuteCommand(cmdId, argv, source)
	source = source or "slash"
	argv = argv or {}
	
	-- Idempotency check
	if self.CommandExecutionGuard:IsDuplicate(cmdId, argv) then
		self:Debug("Command ignored (idempotency guard): " .. cmdId)
		return
	end
	
	-- Build execution context
	local ctx = {
		source = source, -- "slash" or "ui"
		sender = self:GetFullPlayerName("player"),
		isOfficer = self:IsOfficerSafe(),
		now = GetTime(),
		print = function(msg) self:Print(msg) end,
		debug = function(msg) self:Debug(msg) end,
	}
	
	-- Dispatch to CommandRegistry
	if not _G.UI_CommandRegistry then
		self:Print("Command system not available.")
		return
	end
	
	local success, error = _G.UI_CommandRegistry:Execute(cmdId, argv, ctx)
	if not success then
		self:Print("Error executing command: " .. tostring(error))
	end
end

function Cerebro:IsOfficerSafe(player)
	-- Wrapper around IsOfficer that never errors
	local success, result = pcall(function()
		return self:IsOfficer(player)
	end)
	
	if not success then
		return false -- Fail closed
	end
	
	return result
end

-- STRICT Guild Master Check (Rank 0 ONLY)
-- NO solo fallback, NO raid leader fallback for destructive commands
-- Per SECURITY_MODEL: Factory Reset and Restore are GM-only by default
function Cerebro:IsGuildMaster(player)
	local target = player or "player"
	
	-- MUST be in a guild - no guild = no GM = no access
	if not IsInGuild() then
		return false
	end
	
	local fullName = self:GetFullPlayerName(target)
	local _, _, rankIndex = GetGuildInfo(target)
	
	-- Fallback to roster scan if rankIndex is nil
	if not rankIndex then
		for i = 1, GetNumGuildMembers() do
			local name, _, rIdx = GetGuildRosterInfo(i)
			if name and self:GetFullPlayerName(name) == fullName then
				rankIndex = rIdx
				break
			end
		end
	end
	
	-- Rank 0 is Guild Master - the ONLY condition that matters
	return rankIndex == 0
end

-- Safe wrapper for IsGuildMaster (fail closed)
function Cerebro:IsGuildMasterSafe(player)
	local success, result = pcall(function()
		return self:IsGuildMaster(player)
	end)
	
	if not success then
		return false -- Fail closed
	end
	
	return result
end

--[[=============================================
	Guild Scope Verification
	
	Verifies if current character is in the authorized guild.
	See SECURITY_MODEL.md §7 for full documentation.
	
	Returns:
		true  = Access granted (scope matches or bootstrapping)
		false = Access denied (scope mismatch)
		nil   = Pending (API not ready, caller should retry)
===============================================]]

function Cerebro:VerifyGuildScope()
	local scope = self.db.global.scope
	
	-- Bootstrapping: no scope set yet = allow (officer will set it)
	if not scope then
		return true
	end
	
	-- Not in a guild = mismatch
	if not IsInGuild() then
		return false
	end
	
	-- API not ready yet = defer (return nil to indicate "pending")
	local guildName, _, _, _ = GetGuildInfo("player")
	if not guildName then
		return nil -- Caller should retry or show "Loading..."
	end
	
	-- Primary check: GUID (most stable identifier)
	if scope.guildGUID and GetGuildGUID then
		local currentGUID = GetGuildGUID()
		if currentGUID and currentGUID == scope.guildGUID then
			return true
		end
	end
	
	-- Fallback: Name + Realm match
	local currentRealm = self:GetNormalizedRealm()
	if scope.guildName == guildName and scope.realm == currentRealm then
		return true
	end
	
	-- GUID mismatch but name/realm match (guild rename case)
	-- If we have GUID but it doesn't match, and name/realm do match,
	-- allow and auto-heal the GUID to prevent future mismatches
	if scope.guildName == guildName then
		-- Auto-heal: update stored GUID to current (guild rename/transfer)
		if GetGuildGUID then
			local currentGUID = GetGuildGUID()
			if currentGUID and scope.guildGUID ~= currentGUID then
				self:Debug("Scope GUID auto-healed: " .. (scope.guildGUID or "nil") .. " -> " .. currentGUID)
				scope.guildGUID = currentGUID
			end
		end
		return true
	end
	
	return false
end


function Cerebro:IsOfficer(player)
	local target = player or "player"
	local fullName = self:GetFullPlayerName(target)

	-- 1. Guild Security (Highest Priority)
	if IsInGuild() then
		local _, rankName, rankIndex = GetGuildInfo(target)
		
		-- Debug logging for troubleshooting
		-- self:Debug(string.format("IsOfficer Check: Guild=%s, Rank=%s, Index=%s", 
		-- 	tostring(guildName), tostring(rankName), tostring(rankIndex)))
		
		-- Fallback to roster scan if rankIndex is nil (common for non-grouped players or data lag)
		if not rankIndex then
			for i = 1, GetNumGuildMembers() do
				local name, rName, rIdx = GetGuildRosterInfo(i)
				if name and self:GetFullPlayerName(name) == fullName then
					rankName = rName
					rankIndex = rIdx
					break
				end
			end
		end

		if rankIndex then
			-- Rank 0 is ALWAYS Guild Master
			if rankIndex == 0 then return true end
			
			-- Check for common officer rank keywords
			local rLower = (rankName or ""):lower()
			if rLower:find("officer") or rLower:find("admin") or rLower:find("lead") or rLower:find("gm") then
				return true
			end
			
			-- If they are in the guild and we FOUND their rank, but it's not an officer rank,
			-- we STRICTLY reject them. This prevents random raiders from being promoted to 
			-- raid assistant and "hacking" the system.
			return false
		end
	end

	-- 2. Fallback for non-guild groups (PUGs) - raid/party leader/assistant only
	if IsInRaid() then
		for i = 1, GetNumGroupMembers() do
			local name, rank = GetRaidRosterInfo(i)
			if name and self:GetFullPlayerName(name) == fullName then
				return (rank >= 1) -- 1=Assistant, 2=Leader
			end
		end
	elseif IsInGroup() then
		return UnitIsGroupLeader(target) or UnitIsGroupAssistant(target)
	end

	-- No officer status detected - fail closed
	-- NOTE: Solo testing no longer auto-grants officer status
	-- Use /cb debug or join a test guild with officer rank for testing
	return false
end

-- Checks if a player has a specific guild rank (by name)
function Cerebro:IsGuildRank(player, rankName)
	if not IsInGuild() then return false end
	
	-- Ensure guild roster is loaded
	if GetNumGuildMembers() == 0 then
		if C_GuildInfo and C_GuildInfo.GuildRoster then
			C_GuildInfo.GuildRoster()
		end
		return false
	end
	
	local targetRank = self:GetPlayerGuildRank(player)
	return targetRank and targetRank:lower() == rankName:lower()
end

-- Cache for guild ranks to avoid constant rescanning
Cerebro.guildRankCache = {
	data = {},
	lastUpdate = 0,
}

-- Returns the guild rank name for a given player
function Cerebro:GetPlayerGuildRank(player)
	if not IsInGuild() then return nil end

	-- Use short player name for cache key
	local shortPlayerName = self:GetShortPlayerName(player)

	-- Check cache, refresh every 5 minutes (300s)
	if self.guildRankCache.data[shortPlayerName] and (time() - self.guildRankCache.lastUpdate < 300) then
		return self.guildRankCache.data[shortPlayerName]
	end

	-- Update cache if stale
	self.guildRankCache.data = {}
	self.guildRankCache.lastUpdate = time()
	
	if GetNumGuildMembers() == 0 then
		if C_GuildInfo and C_GuildInfo.GuildRoster then
			C_GuildInfo.GuildRoster()
		end
	end
	
	for i = 1, GetNumGuildMembers() do
		local name, rankName = GetGuildRosterInfo(i)
		if name then
			self.guildRankCache.data[self:GetShortPlayerName(name)] = rankName
		end
	end

	return self.guildRankCache.data[shortPlayerName]
end

function Cerebro:IsMasterLooter()
	local lootMethod, _, raidIndex = GetLootMethod()
	return lootMethod == "master" and raidIndex == 0
end

function Cerebro:GetPlayerFullName(unit)
	unit = unit or "player"
	local name, realm = UnitName(unit)
	realm = realm or GetRealmName()
	return name .. "-" .. realm
end

function Cerebro:Debug(msg)
	if self.db.global.config.debug then
		self:Print("|cff888888[DEBUG]|r " .. msg)
	end
end



--[[=============================================
	Slash Commands
===============================================]]

function Cerebro:SlashCommand(input)
	local args = {}
	
	-- Quote-aware argument parsing
	-- Handles both "quoted strings" for arguments with spaces (e.g., "PlayerName-Area 52")
	-- and regular space-separated arguments
	local i = 1
	local len = #input
	
	while i <= len do
		-- Skip leading whitespace
		while i <= len and input:sub(i, i):match("%s") do
			i = i + 1
		end
		
		if i > len then break end
		
		local char = input:sub(i, i)
		
		if char == '"' then
			-- Quoted string - find closing quote
			local startPos = i + 1
			local endPos = input:find('"', startPos, true)
			
			if endPos then
				table.insert(args, input:sub(startPos, endPos - 1))
				i = endPos + 1
			else
				-- No closing quote, treat rest as single arg
				table.insert(args, input:sub(startPos))
				break
			end
		else
			-- Unquoted - find next whitespace
			local startPos = i
			while i <= len and not input:sub(i, i):match("%s") do
				i = i + 1
			end
			table.insert(args, input:sub(startPos, i - 1))
		end
	end

	local cmd = args[1] and args[1]:lower() or ""
	table.remove(args, 1) -- Remove command token, keep rest as argv

	-- Command mapping table (alias -> registry ID)
	local cmdMap = {
		[""] = "toggle_ui",
		["help"] = "help",
		["show"] = "toggle_ui",
		["ui"] = "toggle_ui",
		["wishlist"] = "show_wishlist",
		["wl"] = "show_wishlist",
		["standings"] = "show_standings",
		["history"] = "show_history",
		["config"] = "show_config",
		["options"] = "show_config",
		["verify"] = "run_verification",
		["debug"] = "toggle_debug",
		["discord"] = "discord_export",
		["start"] = "start_session",
		["end"] = "end_session",
		["check"] = "prep_check",
		["prep"] = "prep_check",
		["add"] = "award_points",
		["sub"] = "subtract_points",
		["subtract"] = "subtract_points",
		["readiness"] = "award_readiness",
		["admit"] = "admit_player",
		["fixdb"] = "fix_database",
		["delete"] = "delete_player",
		["factoryreset"] = "factory_reset",
		["restore"] = "restore_backup",
		["backup"] = "backup_status",
		["sync"] = "sync_data",
		["status"] = "show_status",
		["selftest"] = "self_test",
		["testmplus"] = "test_mplus",
		["testenv"] = "test_env",
		["quarantine"] = "quarantine_status",
		["versioncheck"] = "version_check",
		["vercheck"] = "version_check",
		["purge"] = "purge_player",
		["purgetest"] = "purge_test_players",
		["attendance"] = "attendance_summary",
		["testseasonreset"] = "test_season_reset",
		["freshstart"] = "fresh_start",
		["seasonreset"] = "season_reset",
		["intro"] = "show_intro",
		["testui"] = "show_test_ui",
		["purgepending"] = "purge_pending",
	}

	local cmdId = cmdMap[cmd]
	if not cmdId then
		self:Print("Unknown command. Type /cb help for available commands.")
		return
	end

	-- Execute via unified dispatcher
	self:ExecuteCommand(cmdId, args, "slash")
end

function Cerebro:ShowHelp()
	self:Print("=== Cerebro Commands ===")
	self:Print("/cb show - Toggle main UI")

	self:Print("/cb wishlist - Manage your wishlist")
	self:Print("/cb standings - View EP/GP Standings")
	self:Print("/cb history - View history")
	if self.modules.DiscordExport then
		self:Print("/cb discord - Export data for Discord")
	end
	self:Print("/cb config - Open options")
	self:Print("/cb debug - Toggle debug mode")
	self:Print("/cb status - Show addon status")
	self:Print("/cb selftest - Run health checks")
	self:Print("/cb intro - Re-show onboarding wizard")
	self:Print("/cb freshstart YES - Reset local data and re-sync")
	self:Print("/cb testui - Open UI development sandbox")
	self:Print(" ")
	self:Print("|cffff8800Officer Commands:|r")
	self:Print("/cb start [name] - Start raid session")
	self:Print("/cb end - End session")
	self:Print("/cb check - Run prep check")
	self:Print("/cb add [p] [amt] (ep/gp) - Add EP/GP")
	self:Print("/cb sub [p] [amt] (ep/gp) - Subtract EP/GP")
	self:Print("/cb delete [p] - Delete player data")
	self:Print("/cb admit [p] - Admit player to Core")
	self:Print("/cb readiness award [p] [amt] - Award Readiness")
	self:Print("/cb sync - Force data sync")
	self:Print("/cb fixdb - Run database cleanup")
	self:Print("/cb backup - View backup status")
	self:Print(" ")
	self:Print("|cffff0000GM Only Commands:|r")
	self:Print("/cb factoryreset YES - Wipe database (creates backup)")
	self:Print("/cb restore YES - Restore from backup")
end



--[[=============================================
	Communication Handler
===============================================]]

function Cerebro:OnCommReceived(prefix, message, distribution, sender)
	-- Deserialize message
	local success, msgType, data = self:Deserialize(message)

	if not success then
		self:Debug("Failed to deserialize message from " .. sender)
		return
	end

	-- Route to Communication module
	if self.modules.Communication then
		self.modules.Communication:HandleMessage(msgType, data, sender)
	end
end

--[[=============================================
	Custom Events (for module communication)
===============================================]]

function Cerebro:SendMessage(event, ...)
	-- Fire custom event for modules to listen to
	-- This uses Ace3's message system, not to be confused with addon comms
	self:Debug("Event: " .. event)

	-- Notify all modules
	for name, module in pairs(self.modules) do
		if module.OnEvent then
			module:OnEvent(event, ...)
		end
	end
end

--[[=============================================
	Verification / Tests
===============================================]]

function Cerebro:RunVerification()
	self:Print("Running EPGP Logic Verification...")
	
	-- 1. Setup Test Player
	local testName = "TestPlayer-Realm"
	local p = self.Player:New(testName)
	p:GetData().ep = 0
	p:GetData().gp = 0
	
	-- 2. Test EP Award
	p:AddEP(100)
	local epEnd = p:GetEP()
	if epEnd == 100 then self:Print("PASS: EP Award (0->100)") else self:Print("FAIL: EP Award") end
	
	-- 3. Test GP Charge
	p:GetData().gp = 0 -- Reset
	p:AddGP(50)
	local rawGP = p:GetRawGP()
	
	if rawGP == 50 then self:Print("PASS: GP Charge (0->50)") else self:Print("FAIL: GP Charge") end
	
	-- 4. Test PR
	-- PR = EP / GP. (Using effective GP = max(Raw, Base))
	p:GetData().ep = 1000
	p:GetData().gp = 100 -- Assuming BaseGP < 100
	p:UpdatePR()
	local pr = p:GetPR()
	if math.abs(pr - 10.0) < 0.01 then self:Print("PASS: PR Calc (1000/100=10)") else self:Print("FAIL: PR Calc got "..pr) end
	
	-- 5. Test Decay
	p:GetData().ep = 1000
	p:GetData().gp = 1000
	p:Decay(0.1) -- 10%
	
	local decayedEP = p:GetEP()
	local decayedGP = p:GetRawGP()
	
	if decayedEP == 900 and decayedGP == 900 then 
		self:Print("PASS: Decay 10%") 
	else 
		self:Print("FAIL: Decay (Got "..decayedEP.."/"..decayedGP..")") 
	end
	
	self:Print("Verification Complete.")
end
