--[[
	Cerebro - Mythic+ Module
	
	PURPOSE:
	Tracks Mythic+ dungeon completions and awards Readiness EP based on keystone level
	and guild group composition. This module provides an alternative EP source outside
	of raid progression, subject to weekly caps enforced by the EPGP module.
	
	RESPONSIBILITIES:
	1. Challenge Mode Detection - Listen for M+ start/complete events
	2. Group Snapshotting - Capture party composition and guild member count
	3. EP Calculation - Award based on level and guild participation
	4. Weekly Cap Delegation - Defer to EPGP:AwardReadinessEP for cap enforcement
	5. Run History - Archive completed runs for analytics
	
	CONTRACTS (Code-Derived + Behavior Change):
	
	# CHALLENGE_MODE_START Event Handler
	  - Snapshots party composition via UnitExists("player"/"party1-4")
	  - Counts guild members (UnitIsInMyGuild check)
	  - Stores activeRun state with startTime, level, group, guildCount
	  
	# CHALLENGE_MODE_COMPLETED Event Handler
	  - Validates activeRun exists
	  - Generates unique run ID: "mplus_" + timestamp
	  - Uses completionInfo.time (server-authoritative) for duration
	  - Archives to db.global.mplusRuns
	  - Calculates and awards EP to all guild members in group
	  - Clears activeRun state
	  
	# CalculateEPAward(level, guildCount) -> totalEP, baseEP, levelEP, bonusEP
	  - Formula v1.2: (Base + clampedLevel * PerLevel) + GuildBonus(guildCount)
	  - clampedLevel = min(level, MaxLevel)
	  - Min Level Gate: Runs below MinLevel (default 2) award 0 EP
	  - Guild Bonus: Per-tier config (mplusGuildBonus2..5), percentage or flat
	  - No penalty for untimed runs (onTime not used in calculation)
	  - Returns: Total EP, Base EP, Level Bonus EP, Guild Bonus EP (for breakdown)
	  
	AWARD FORMULA v1.2 (Threshold-Based):
	```
	if Level < MinLevel then 0
	TotalEP = (Base + min(Level, MaxLevel) * PerLevel) + GuildBonus(guildCount)
	```
	
	Example (defaults: Base=20, PerLevel=5, MaxLevel=10, MinLevel=2):
	- Level 1 key (below min): 0 EP
	- Solo (+2 key): 20 + (2*5) + 0 = 30 EP
	- Full guild (+10 key, 5 guildies, bonus5=10%): 20 + (10*5) + 7 = 77 EP
	
	INVARIANTS:
	1. Run IDs must be unique (enforced via timestamp-based generation)
	2. Guild count is capped at 4 for bonus calculation
	3. Keystone level is capped at MaxLevel for EP calculation
	4. Timed and untimed runs award identical EP (no penalty)
	5. Awards delegated to EPGP:AwardReadinessEP (weekly cap enforcement)
	
	DESIGN DECISIONS:
	- Guild bonus uses per-tier config: mplusGuildBonus2..5 (percentage or flat)
	- No untimed penalty (previous 50% penalty removed per behavior change)
	- Weekly cap handled by EPGP module (readinessCap config)
	- Run history preserved for analytics (not used for idempotency)
	
	SECURITY MODEL:
	This module does not enforce authorization for EP awards. It trusts that
	the WoW client event system is authoritative (CHALLENGE_MODE_COMPLETED only
	fires for actual completions). Configuration changes (mplusBase, etc.) are
	protected by officer checks in UI/Config.lua.
	
	EXTERNAL DEPENDENCIES:
	- Modules/EPGP.lua (AwardReadinessEP for weekly cap enforcement)
	- Database.lua (config.epgp.mplus* settings)
	- Utilities.lua (GetFullPlayerName)
	- WoW API: C_ChallengeMode, UnitExists, UnitName, UnitClass, UnitIsInMyGuild
	
	TEST COVERAGE:
	- TEST-MPLUS-001: Run state management, ID generation
	- TEST-MPLUS-002: EP calculation with level/guild scaling
	- TEST-MPLUS-003: Guild count gating and bonus scaling
]]--

local AddonName, PrivateTable = ...
local Cerebro = LibStub("AceAddon-3.0"):GetAddon(AddonName)
local MythicPlus = Cerebro:NewModule("MythicPlus", "AceEvent-3.0", "AceTimer-3.0")

MythicPlus.activeRun = nil

function MythicPlus:OnInitialize()
	self:Debug("MythicPlus module initialized")
end

function MythicPlus:OnEnable()
	self:RegisterEvent("CHALLENGE_MODE_START")
	self:RegisterEvent("CHALLENGE_MODE_COMPLETED")
	self:RegisterEvent("INSPECT_READY")
	
	if not Cerebro.db.global.mplusRuns then
		Cerebro.db.global.mplusRuns = {}
	end
	
	-- Check active run
	if C_ChallengeMode.GetActiveChallengeMapID() then
		self:CHALLENGE_MODE_START()
	end
end

function MythicPlus:Debug(msg)
	Cerebro:Debug("|cff00ff00[M+]|r " .. msg)
end

-- M+ Rating cache to avoid repeated API calls
MythicPlus.ratingCache = {}

function MythicPlus:GetUnitMPlusRating(unit)
	if not UnitExists(unit) or not UnitIsPlayer(unit) then return 0 end
	local guid = UnitGUID(unit)
	if guid and self.ratingCache[guid] then return self.ratingCache[guid] end
	
	local summary = C_PlayerInfo.GetPlayerMythicPlusRatingSummary(unit)
	local score = (summary and summary.currentSeasonScore) or 0
	if guid then self.ratingCache[guid] = score end
	return score
end

-- Force refresh M+ rating, bypassing cache (called after INSPECT_READY)
-- Contract: Returns current season score, updates cache with fresh value
function MythicPlus:RefreshUnitMPlusRating(unit)
	if not UnitExists(unit) or not UnitIsPlayer(unit) then return 0 end
	local summary = C_PlayerInfo.GetPlayerMythicPlusRatingSummary(unit)
	local score = (summary and summary.currentSeasonScore) or 0
	local guid = UnitGUID(unit)
	if guid then self.ratingCache[guid] = score end
	return score
end

function MythicPlus:CHALLENGE_MODE_START()
	local mapID = C_ChallengeMode.GetActiveChallengeMapID()
	if not mapID then return end
	
	local level = C_ChallengeMode.GetActiveKeystoneInfo()
	
	-- Snapshot group
	local group = {}
	local guildies = 0
	local partyNames = {}  -- Track ALL party members for runKey
	
	local units = {"player", "party1", "party2", "party3", "party4"}
	for _, unit in ipairs(units) do
		if UnitExists(unit) then
			local unitName = UnitName(unit)
			if unitName then
				local name = Cerebro:GetFullPlayerName(unitName)
				table.insert(partyNames, name)  -- All party members for runKey
				local isGuild = UnitIsInMyGuild(unit) or Cerebro:IsGuildRank(name, "Alt")
				local role = UnitGroupRolesAssigned(unit)
				
				if isGuild then guildies = guildies + 1 end
				
				table.insert(group, {
					name = name,
					class = select(2, UnitClass(unit)) or "UNKNOWN",
					role = (role ~= "NONE" and role) or "DMG",
					specID = 0,  -- Populated by Inspect Queue
					mplusRating = self:GetUnitMPlusRating(unit),
					isGuild = isGuild
				})
			end
		end
	end
	
	self.activeRun = {
		startTime = GetServerTime() or time(),  -- Server time for cross-client consistency
		mapID = mapID,
		level = level,
		group = group,
		guildCount = guildies,
		partyNames = partyNames  -- Store full party for runKey
	}
	
	self:Debug(string.format("Run Started: Level %d with %d guildies.", level, guildies))
	
	-- Start Inspect Queue for spec ID fetching
	self:StartInspectQueue()
end

-- Inspect Queue System for async spec ID fetching
function MythicPlus:StartInspectQueue()
	if self.inspectTimerHandle then self:CancelTimer(self.inspectTimerHandle) end
	self.inspectQueue = {}
	self.inspectGuidMap = {}
	
	for i = 1, 4 do
		local unit = "party" .. i
		if UnitExists(unit) and UnitIsPlayer(unit) then
			table.insert(self.inspectQueue, unit)
			self.inspectGuidMap[unit] = UnitGUID(unit)
		end
	end
	
	if #self.inspectQueue > 0 then
		self.inspectTimerHandle = self:ScheduleRepeatingTimer("ProcessInspectQueue", 2)
	end
end

function MythicPlus:ProcessInspectQueue()
	-- Guard: combat/arena
	if InCombatLockdown() or C_PvP.IsArena() then return end
	if #self.inspectQueue == 0 then
		self:CancelTimer(self.inspectTimerHandle)
		self.inspectTimerHandle = nil
		return
	end
	local unit = table.remove(self.inspectQueue, 1)
	if UnitExists(unit) and UnitIsConnected(unit) then
		NotifyInspect(unit)
	end
end

function MythicPlus:INSPECT_READY(event, guid)
	if not self.activeRun or not self.inspectGuidMap then return end
	for unit, expectedGuid in pairs(self.inspectGuidMap) do
		if expectedGuid == guid then
			local specID = GetInspectSpecialization(unit)
			-- Refresh M+ rating now that player profile data is loaded
			local rating = self:RefreshUnitMPlusRating(unit)
			for _, member in ipairs(self.activeRun.group) do
				if UnitGUID(unit) == guid then
					member.specID = specID or 0
					member.mplusRating = rating
					self:Debug(string.format("Updated %s: SpecID=%d, M+ Rating=%d", member.name, specID or 0, rating))
					break
				end
			end
			break
		end
	end
end


-- Calculate EP award based on keystone level and guild group composition
-- Contract: TotalEP = (Base + LevelBonus * PerLevel) + (GuildBonus * min(max(0, GuildCount - 1), 4))
-- Level scaling starts at +3 (Level 2 is the floor, awards Base only)
-- Guild bonus excludes the player (addon runner) from count
-- No penalty for untimed runs (behavior change: timed and untimed award equal EP)
-- Returns: total, baseEP, levelEP, bonusEP for breakdown display
function MythicPlus:CalculateEPAward(level, guildCount)
	local config = Cerebro.db.global.config.epgp or {}
	local base = config.mplusBase or 20
	local perLevel = config.mplusPerLevel or 5
	local minLevel = config.mplusMinLevel or 2
	local maxLevel = config.mplusMaxLevel or 10
	
	-- Min Level Gate: If below min level, no EP is awarded
	if level < minLevel then
		return 0, 0, 0, 0
	end

	-- Cap level for scaling calculation
	local clampedLevel = math.min(level, maxLevel)
	
	-- Base EP (constant)
	local baseEP = base
	
	-- Level bonus EP (Direct multiplication, no subtraction offset)
	local levelEP = clampedLevel * perLevel
	
	-- Calculate total pre-bonus
	local subTotal = baseEP + levelEP
	
	-- Guild Bonus (Flexible: Percentage "10%" or Flat "50")
	-- Logic: Starts at 2 guild members (1 member is the base run)
	local bonusString = "0"
	if guildCount >= 2 and guildCount <= 5 then
		bonusString = config["mplusGuildBonus" .. guildCount] or "0"
	elseif guildCount > 5 then
		bonusString = config.mplusGuildBonus5 or "0"
	end
	
	local bonusEP -- Calculated below based on percentage or flat value
	
	-- Check for percentage
	if type(bonusString) == "string" and bonusString:find("%%") then
		local pct = tonumber(bonusString:match("([%d%.]+)")) or 0
		bonusEP = math.floor(subTotal * (pct / 100))
	else
		-- Treat as flat value (handle string or number input)
		local flat = tonumber(bonusString) or 0
		bonusEP = math.floor(flat)
	end
	
	local total = subTotal + bonusEP
	
	return math.floor(total), baseEP, levelEP, bonusEP
end

-- Generate deterministic Run Key for deduplication
-- Contract: Same run = same runKey across all clients
-- Format: mplus:<mapID>:<level>:<startBucket>:<sortedPartyNames>
-- Uses run start time (server time) and full party names for uniqueness
function MythicPlus:GenerateRunKey(mapID, level, partyNames)
	local sortedNames = {unpack(partyNames)}  -- Copy to avoid mutating original
	table.sort(sortedNames)
	local partyStr = table.concat(sortedNames, ",")
	-- 60-second bucket on start time handles minor event propagation variance
	local startBucket = math.floor((self.activeRun and self.activeRun.startTime or GetServerTime() or time()) / 60)
	return string.format("mplus:%d:%d:%d:%s", mapID, level, startBucket, partyStr)
end

-- Check if we already have a transaction for this runKey and player
-- This prevents duplicate awards when multiple clients process the same run
local function HasExistingRunKeyAward(runKey, playerName)
	for _, t in ipairs(Cerebro.db.global.transactions) do
		if t.details and t.details.runKey == runKey and t.player == playerName then
			return true
		end
	end
	return false
end

-- Challenge Mode completion handler
-- Contract: Officers award ALL guild members (Verified). Members award SELF-ONLY (Unverified claim).
-- Phase 1: Generates runKey and rich metadata for deduplication.
function MythicPlus:CHALLENGE_MODE_COMPLETED()
	if not self.activeRun then return end
	
	-- GetCompletionInfo deprecated in 11.0.5, replaced with GetChallengeCompletionInfo (returns struct)
	local completionInfo = C_ChallengeMode.GetChallengeCompletionInfo()
	if not completionInfo then return end
	local level = completionInfo.level
	local onTime = completionInfo.onTime
	local keystoneUpgradeLevel = completionInfo.keystoneUpgradeLevels
	
	-- Build party and guild member lists
	local guildieNames = {}
	local partyNames = self.activeRun.partyNames or {}  -- Use captured party names
	for _, member in ipairs(self.activeRun.group) do
		if member.isGuild then
			table.insert(guildieNames, member.name)
		end
	end
	
	-- Generate deterministic runKey using full party names
	local runKey = self:GenerateRunKey(self.activeRun.mapID, self.activeRun.level, partyNames)
	
	-- Use completionInfo.time (server-authoritative) instead of calculating from activeRun.startTime
	-- This prevents incorrect duration when a player reloads UI during a run (which resets startTime)
	-- NOTE: completionInfo.time is in MILLISECONDS, convert to seconds for consistent storage
	local serverDuration = completionInfo.time and math.floor(completionInfo.time / 1000) or (time() - self.activeRun.startTime)
	
	local runData = {
		id = "mplus_" .. time(),
		timestamp = time(),
		duration = serverDuration,
		mapID = self.activeRun.mapID,
		level = self.activeRun.level,
		success = onTime,
		upgrade = keystoneUpgradeLevel,
		group = self.activeRun.group,
		guildCount = self.activeRun.guildCount,
		runKey = runKey,
		status = Cerebro:IsOfficer() and "Verified" or "Claimed"  -- Officer runs are auto-verified
	}
	
	-- Calculate EP using snapshot values and store breakdown in runData for UI tooltip
	local epAmount, baseEP, levelEP, bonusEP = self:CalculateEPAward(runData.level, runData.guildCount)
	runData.baseEP = baseEP
	runData.levelEP = levelEP
	runData.bonusEP = bonusEP
	runData.totalEP = epAmount
	
	table.insert(Cerebro.db.global.mplusRuns, runData)
	
	
	-- Format breakdown string for visibility
	local breakdownStr = string.format("[Base:%d Lvl:%d Guild:%d]", baseEP, levelEP, bonusEP)
	local reasonStr = string.format("M+ %d (%s) %s", level, onTime and "Timed" or "Untimed", breakdownStr)
	
	-- Build rich details for transaction metadata
	local details = {
		runKey = runKey,
		mapID = runData.mapID,
		level = runData.level,
		onTime = onTime,
		upgrade = keystoneUpgradeLevel or 0,
		guildCount = runData.guildCount,
		guildies = guildieNames,
		party = partyNames,
		-- Breakdown for History tab display
		baseEP = baseEP,
		levelEP = levelEP,
		bonusEP = bonusEP
	}
	
	if epAmount > 0 then
		local epgpMod = Cerebro:GetModule("EPGP")
		local myName = Cerebro:GetPlayerFullName()
		
		-- v1.0+ Sync Architecture: Everyone self-awards only
		-- Officers NO LONGER award all guild members; they award themselves only
		-- This reduces sync conflicts and ensures consistent EP/GP across all clients
		-- Transaction will be synced to officers via receipts
		
		if not HasExistingRunKeyAward(runKey, myName) then
			local actualAwarded = epgpMod:AwardReadinessEP(myName, epAmount, reasonStr, details)
			
			-- Store actual awarded amount on runData for consistent UI display
			runData.awardedEP = actualAwarded
			runData.wasCapped = (actualAwarded < epAmount)
			
			self:Debug("Awarded self: " .. actualAwarded .. " of " .. epAmount .. " Readiness EP." .. (runData.wasCapped and " (CAPPED)" or ""))
			if runData.wasCapped then
				Cerebro:Print(string.format("|cff00ff00+%d EP|r |cffff8800(Capped)|r M+ %d %s — Base:%d + Lvl:%d + Guild:%d", actualAwarded, level, onTime and "(Timed)" or "(Untimed)", baseEP, levelEP, bonusEP))
			else
				Cerebro:Print(string.format("|cff00ff00+%d EP|r M+ %d %s — Base:%d + Lvl:%d + Guild:%d", actualAwarded, level, onTime and "(Timed)" or "(Untimed)", baseEP, levelEP, bonusEP))
			end
		else
			self:Debug("Skipped self-award for " .. myName .. " (runKey already awarded)")
			runData.awardedEP = 0
			runData.wasCapped = false
		end
	else
		self:Debug("No EP awarded (amount calculated as 0).")
		runData.awardedEP = 0
		runData.wasCapped = false
	end
	
	-- Push to officers AFTER award so runData includes awardedEP/wasCapped (V2 RPC)
	local v2 = Cerebro:GetModule("CommunicationV2", true)
	if v2 and v2.SendRPC then
		if IsInRaid() then
			-- Suppress M+ comms while in raid — queue for later
			self._pendingMPlusPush = self._pendingMPlusPush or {}
			table.insert(self._pendingMPlusPush, runData)
			self:Debug("M+ push queued (in raid), will send on raid exit")
		else
			v2:SendRPC("MPLUS_PUSH", runData, "GUILD")
		end
	end
	
	self.activeRun = nil
end

-- Simulate a Mythic+ completion for testing EP calculations
-- Contract: Dry run by default (prints calculation, no DB write)
-- Usage: /cb testmplus run [level] [guildies] [--untimed] [--live] [--target Player-Realm]
function MythicPlus:SimulateRun(level, guildCount, timed, isLive, targetPlayer)
	-- Redirect to unified GenerateTestRuns function
	self:GenerateTestRuns(1, isLive)
end

-- Inject test M+ runs for UI testing (does NOT award EP)
-- Legacy function - redirects to unified GenerateTestRuns
function MythicPlus:InjectTestRuns(count)
	self:GenerateTestRuns(count or 5, false)
end

-- ============================================================================
-- Unified Test Run Generator
-- Usage: /cb testmplus [count] [--live]
-- 
-- Generates randomized M+ test runs with proper party composition.
-- Without --live: Adds runs to My Progress for UI testing (no EP award)
-- With --live: Also awards EP to the ledger for each guild member
-- ============================================================================
function MythicPlus:GenerateTestRuns(count, isLive)
	count = count or 1
	isLive = (isLive == true)
	
	if not Cerebro.db.global.mplusRuns then
		Cerebro.db.global.mplusRuns = {}
	end
	
	-- Player info (fetched once for all runs)
	local myName = Cerebro:GetFullPlayerName(UnitName("player"))
	local myRating = self:RefreshUnitMPlusRating("player")
	local myClass = select(2, UnitClass("player"))
	local myRealm = GetRealmName():gsub("%s", "")
	
	Cerebro:Print(string.format("|cff00ff00[M+ Test]|r Your M+ Rating: %d", myRating))
	
	-- TWW Season 3 Dungeon Pool (patch 11.2)
	local dungeons = {
		{id = 2660, name = "Ara-Kara, City of Echoes"},
		{id = 2662, name = "The Dawnbreaker"},
		{id = 2651, name = "The Stonevault"},
		{id = 2649, name = "Priory of the Sacred Flame"},
		{id = 2669, name = "Operation: Floodgate"},
		{id = 2773, name = "Eco-Dome Al'dani"},
		{id = 2287, name = "Halls of Atonement"},
		{id = 2441, name = "Tazavesh: Streets of Wonder"},
	}
	
	-- Class pools for random composition
	local tankClasses = {"WARRIOR", "PALADIN", "DEATHKNIGHT", "MONK", "DRUID", "DEMONHUNTER"}
	local healerClasses = {"PRIEST", "PALADIN", "SHAMAN", "DRUID", "MONK", "EVOKER"}
	local dpsClasses = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "MONK", "DRUID", "DEMONHUNTER", "EVOKER"}
	
	local baseTime = time()
	local epgpMod = isLive and Cerebro:GetModule("EPGP", true) or nil
	local totalEPAwarded = 0
	
	for i = 1, count do
		-- Randomize run parameters
		local level = math.random(10, 22)
		local dungeon = dungeons[math.random(#dungeons)]
		local timed = math.random() > 0.2 -- 80% chance timed
		local upgrade = timed and math.random(1, 3) or 0
		local duration = math.random(1200, 2400) -- 20-40 mins
		local guildCount = 5 -- Always full party for consistency
		
		-- Random class selection for this run
		local randomTank = tankClasses[math.random(#tankClasses)]
		local randomHealer = healerClasses[math.random(#healerClasses)]
		local randomDPS1 = dpsClasses[math.random(#dpsClasses)]
		local randomDPS2 = dpsClasses[math.random(#dpsClasses)]
		
		-- Build party with proper composition: Tank, Healer, Player(DPS), DPS, DPS
		local group = {
			{name = "TestTank-" .. myRealm, class = randomTank, role = "TANK", mplusRating = 1800 + math.random(200), isGuild = true},
			{name = "TestHealer-" .. myRealm, class = randomHealer, role = "HEALER", mplusRating = 1600 + math.random(200), isGuild = true},
			{name = myName, class = myClass, role = "DAMAGER", mplusRating = myRating, isGuild = true},
		}
		
		-- Always add 4th and 5th DPS for full 5-member party
		table.insert(group, {name = "TestDPS1-" .. myRealm, class = randomDPS1, role = "DAMAGER", mplusRating = 1500 + math.random(200), isGuild = true})
		table.insert(group, {name = "TestDPS2-" .. myRealm, class = randomDPS2, role = "DAMAGER", mplusRating = 1400 + math.random(200), isGuild = true})
		
		-- Calculate EP
		local epAmount, baseEP, levelEP, bonusEP = self:CalculateEPAward(level, guildCount)
		
		-- Build partyNames for runKey (all members, not just guildies)
		local partyNames = {}
		for _, m in ipairs(group) do
			table.insert(partyNames, m.name)
		end
		
		local runKey = self:GenerateRunKey(dungeon.id, level, partyNames)
		-- Append unique test suffix to avoid deduplication collisions
		runKey = runKey .. ":test:" .. (baseTime + i)
		
		-- Build run data
		local runData = {
			id = "mplus_test_" .. (baseTime + i),
			timestamp = baseTime - ((i - 1) * 3600 * math.random(1, 24)), -- Spread over time
			duration = duration,
			mapID = dungeon.id,
			level = level,
			success = timed,
			upgrade = upgrade,
			group = group,
			guildCount = guildCount,
			runKey = runKey,
			baseEP = baseEP,
			levelEP = levelEP,
			bonusEP = bonusEP,
			totalEP = epAmount,
			simulated = true, -- Flag for filtering/audit
			status = isLive and "Verified" or "Test"
		}
		
		-- Insert into My Progress history
		table.insert(Cerebro.db.global.mplusRuns, runData)
		Cerebro:Print(string.format("|cff888888[DEBUG] Inserted run %d: +%d %s (key: %s)|r", i, level, dungeon.name, string.sub(runKey, -20)))
		
		-- Push to officers for Guild M+ History tab (V2 RPC)
		local v2 = Cerebro:GetModule("CommunicationV2", true)
		if v2 and v2.SendRPC then
			if IsInRaid() then
				self._pendingMPlusPush = self._pendingMPlusPush or {}
				table.insert(self._pendingMPlusPush, runData)
				self:Debug("M+ push queued (in raid), will send on raid exit")
			else
				v2:SendRPC("MPLUS_PUSH", runData, "GUILD")
			end
		end
		
		-- Award EP if --live mode
		if isLive and epgpMod then
			local reasonStr = string.format("M+ +%d %s (Test) [Base:%d Lvl:%d Guild:%d]", 
				level, timed and "Timed" or "Untimed", baseEP, levelEP, bonusEP)
			
			for _, member in ipairs(group) do
				if member.isGuild then
					epgpMod:AwardReadinessEP(member.name, epAmount, reasonStr, runData)
				end
			end
			totalEPAwarded = totalEPAwarded + (epAmount * guildCount)
		end
	end
	
	-- Summary output
	if isLive then
		Cerebro:Print(string.format("|cff00ff00[LIVE]|r Generated %d test run(s) and awarded EP.", count))
		Cerebro:Print("|cff888888Tip: Use /cb sub [player] [amount] ep to revert if needed.|r")
	else
		Cerebro:Print(string.format("|cff00ff00[Test]|r Generated %d test run(s) in My Progress.", count))
		Cerebro:Print("|cff888888Use --live to also award EP to the ledger.|r")
	end
	
	-- Trigger My Progress refresh
	self:SendMessage("Cerebro_MPlus_Update")
end

-- ============================================================================
-- Award Test EP to Specific Player
-- Usage: /cb testmplus target Player-Realm [--live]
-- 
-- Awards simulated M+ EP to a specific player for testing EP sync.
-- Requires: Officer + Dev Mode
-- ============================================================================
function MythicPlus:AwardTargetEP(targetPlayer, isLive)
	-- Validate target format
	if not targetPlayer or not string.find(targetPlayer, "-") then
		Cerebro:Print("|cffff0000Error:|r Target must be in Player-Realm format.")
		return
	end
	
	local targetFullName = Cerebro:GetFullPlayerName(targetPlayer)
	isLive = (isLive == true)
	
	-- Generate random run parameters
	local level = math.random(10, 22)
	local timed = math.random() > 0.2
	local epAmount, baseEP, levelEP, bonusEP = self:CalculateEPAward(level, 1)
	
	-- Get target's class if possible (for proper display)
	local targetClass = nil
	for i = 1, GetNumGuildMembers() do
		local name, _, _, _, _, _, _, _, _, _, classFileName = GetGuildRosterInfo(i)
		if name and Cerebro:GetFullPlayerName(name) == targetFullName then
			targetClass = classFileName
			break
		end
	end
	targetClass = targetClass or "WARRIOR" -- Fallback
	
	local myRealm = GetRealmName():gsub("%s", "")
	
	-- Class pools for synthetic party members
	local tankClasses = {"WARRIOR", "PALADIN", "DEATHKNIGHT", "MONK", "DRUID", "DEMONHUNTER"}
	local healerClasses = {"PRIEST", "PALADIN", "SHAMAN", "DRUID", "MONK", "EVOKER"}
	local dpsClasses = {"WARRIOR", "HUNTER", "ROGUE", "MAGE", "WARLOCK", "DEMONHUNTER", "EVOKER"}
	
	-- Build full 5-member party (target is one of the DPS)
	local group = {
		{name = "TestTank-" .. myRealm, class = tankClasses[math.random(#tankClasses)], role = "TANK", mplusRating = 1800 + math.random(200), isGuild = true},
		{name = "TestHealer-" .. myRealm, class = healerClasses[math.random(#healerClasses)], role = "HEALER", mplusRating = 1600 + math.random(200), isGuild = true},
		{name = targetFullName, class = targetClass, role = "DAMAGER", mplusRating = 0, isGuild = true},
		{name = "TestDPS1-" .. myRealm, class = dpsClasses[math.random(#dpsClasses)], role = "DAMAGER", mplusRating = 1500 + math.random(200), isGuild = true},
		{name = "TestDPS2-" .. myRealm, class = dpsClasses[math.random(#dpsClasses)], role = "DAMAGER", mplusRating = 1400 + math.random(200), isGuild = true},
	}
	
	-- Random dungeon from TWW S3 pool
	local dungeons = {
		{id = 2660, name = "Ara-Kara, City of Echoes"},
		{id = 2662, name = "The Dawnbreaker"},
		{id = 2651, name = "The Stonevault"},
		{id = 2649, name = "Priory of the Sacred Flame"},
		{id = 2669, name = "Operation: Floodgate"},
	}
	local dungeon = dungeons[math.random(#dungeons)]
	
	-- Build run data with complete fields for sync
	local runData = {
		id = "mplus_target_" .. time(),
		timestamp = time(),
		duration = math.random(1200, 2400), -- 20-40 min
		mapID = dungeon.id,
		mapName = dungeon.name, -- Include name for display
		level = level,
		success = timed,
		upgrade = timed and math.random(1, 3) or 0,
		group = group, -- Full 5-member party!
		guildCount = 5,
		runKey = self:GenerateRunKey(dungeon.id, level, (function() local names = {} for _, m in ipairs(group) do table.insert(names, m.name) end return names end)()),
		baseEP = baseEP,
		levelEP = levelEP,
		bonusEP = bonusEP,
		totalEP = epAmount,
		simulated = true,
		status = isLive and "Verified" or "Test"
	}
	
	-- Print breakdown
	Cerebro:Print(string.format("|cff00ff00[TARGET M+]|r %s", isLive and "LIVE MODE" or "DRY RUN"))
	Cerebro:Print(string.format("  Target: %s", targetFullName))
	Cerebro:Print(string.format("  Level: +%d (%s)", level, timed and "Timed" or "Untimed"))
	Cerebro:Print(string.format("  EP Award: |cff00ff00%d|r", epAmount))
	
	if not isLive then
		Cerebro:Print("  |cff888888Use --live to actually award EP.|r")
		return
	end
	
	-- LIVE MODE: Actually award EP
	local epgpMod = Cerebro:GetModule("EPGP", true)
	if not epgpMod then
		Cerebro:Print("|cffff0000Error:|r EPGP module not loaded.")
		return
	end
	
	local reasonStr = string.format("M+ +%d (Test Sync) [Target:%s]", level, targetFullName)
	epgpMod:AwardReadinessEP(targetFullName, epAmount, reasonStr, runData)
	
	Cerebro:Print(string.format("|cff00ff00LIVE:|r Awarded %d EP to %s", epAmount, targetFullName))
	Cerebro:Print("|cff888888Tip: Use /cb sub " .. targetFullName .. " " .. epAmount .. " ep to revert.|r")
	
	-- Broadcast to target's history if possible (V2 RPC)
	local v2 = Cerebro:GetModule("CommunicationV2", true)
	if v2 and v2.SendRPC then
		local shortName = Cerebro:GetShortPlayerName(targetFullName)
		v2:SendRPC("MPLUS_INJECT", { run = runData }, "WHISPER", shortName)
		Cerebro:Print("|cff888888History entry sent to target.|r")
	end
end

-- Get sorted M+ run history for UI display
-- Filters out simulated/test runs by default
-- Only returns runs where the CURRENT player was a participant
function MythicPlus:GetHistory(limit, includeSimulated)
	local runs = Cerebro.db.global.mplusRuns
	if not runs or #runs == 0 then return {} end
	
	-- Get current player name for filtering
	local myName = Cerebro:GetFullPlayerName(UnitName("player"))
	
	-- Filter: only runs where current player was a participant
	local filtered = {}
	for _, run in ipairs(runs) do
		if includeSimulated or not run.simulated then
			-- Check if current player was in the group
			local wasParticipant = false
			if run.group then
				for _, member in ipairs(run.group) do
					if member.name == myName then
						wasParticipant = true
						break
					end
				end
			end
			if wasParticipant then
				table.insert(filtered, run)
			end
		end
	end
	
	if #filtered == 0 then return {} end
	
	-- Nil-safe sort by timestamp descending
	table.sort(filtered, function(a, b)
		return (a and a.timestamp or 0) > (b and b.timestamp or 0)
	end)
	
	-- Cap results for performance
	limit = math.min(limit or 10, 100)
	local result = {}
	for i = 1, math.min(limit, #filtered) do
		result[i] = filtered[i]
	end
	return result
end

-- Flush queued M+ pushes that were suppressed during raid
-- Called from Core.lua GROUP_ROSTER_UPDATE when leaving raid
function MythicPlus:FlushPendingPushes()
	if not self._pendingMPlusPush or #self._pendingMPlusPush == 0 then return end
	local v2 = Cerebro:GetModule("CommunicationV2", true)
	if not v2 or not v2.SendRPC then return end
	for _, runData in ipairs(self._pendingMPlusPush) do
		v2:SendRPC("MPLUS_PUSH", runData, "GUILD")
	end
	self:Debug("Flushed " .. #self._pendingMPlusPush .. " queued M+ pushes")
	self._pendingMPlusPush = nil
end
