--[[
	Cerebro - Session Module
	
	PURPOSE:
	Manages raid session lifecycle, roster snapshots, and boss kill tracking.
	This module provides the "active state" layer above the EPGP rules engine,
	capturing real-time attendance and orchestrating automated EP awards for
	raid progression.
	
	RESPONSIBILITIES:
	1. Session State Management - Start/End/Active lifecycle
	2. Roster Snapshotting - Capture raid composition at session start
	3. Boss Kill Tracking - Record encounters and award EP
	4. Session Point Tracking - Aggregate session-scoped EP/GP per player
	5. Status Promotion - Auto-promote Pending → Core on first raid attendance
	
	CONTRACTS (Derived from Code):
	
	# StartSession(raidName) -> sessionID
	  - MUST verify IsOfficer() (fail-closed RBAC)
	  - MUST fail if session already active
	  - Generates sessionID: YYYY-MM-DD-### (sequential daily counter)
	  - Auto-detects raidName from instance if not provided
	  - Creates currentSession with startTime, roster snapshot, empty bosses[]
	  - Broadcasts to raid + Communication module
	  - Returns: sessionID string
	  
	# EndSession() -> void
	  - MUST verify IsOfficer() (fail-closed RBAC)
	  - Sets endTime on currentSession
	  - Archives currentSession to db.global.history[sessionID]
	  - Clears db.global.currentSession (state transition to inactive)
	  - Broadcasts end to raid + Communication module
	  
	# RecordBossKill(encounterID, encounterName, difficultyID) -> void
	  - MUST be idempotent: duplicate encounterID in session.bosses[] is ignored
	  - Records boss data: encounterID, name, difficulty, killTime
	  - Increments db.global.totalBossKills (attendance analytics)
	  - Awards fixed EP via EPGP:MassAwardEP():
	    - Normal (14): 50 EP
	    - Heroic (15): 75 EP
	    - Mythic (16): 100 EP
	  - Filter: Only players currently in raid (Cerebro:IsPlayerInRaid())
	  
	# UpdateRoster() -> void
	  - Snapshots current raid roster via Cerebro:GetRaidRoster()
	  - For new players: creates roster entry with joinTime, class, session EP/GP = 0
	  - NOTE: No longer auto-promotes Pending → Core (officer must admit via Officer Tools)
	  - Updates player class in db (sync from roster)
	  
	# OnPointChange(player, amount, type) -> void
	  - Tracks session-scoped EP/GP deltas for analytics
	  - Creates roster entry if player not in snapshot (handles standby/late joins)
	  - Accumulates session.roster[player].sessionEP/sessionGP
	  
	SESSION SCHEMA:
	{
	    sessionID = "YYYY-MM-DD-###",
	    raidName = "Raid Instance Name",
	    startTime = 1234567890,
	    endTime = 1234567899 | nil,  -- nil while active
	    roster = {
	        ["PlayerName-Realm"] = {
	            joinTime = 1234567890,
	            class = "WARRIOR",
	            sessionEP = 150,
	            sessionGP = 200
	        }
	    },
	    bosses = {
	        {
	            encounterID = 12345,
	            name = "Boss Name",
	            difficultyID = 16,
	            killed = true,
	            killTime = 1234567891
	        }
	    }
	}
	
	INVARIANTS:
	1. Only one session can be active at a time (enforced by StartSession)
	2. Boss kills are idempotent within a session (encounterID uniqueness check)
	3. Roster entries persist through session (no removal, only addition)
	4. Session history is append-only (archived sessions never modified)
	
	DESIGN DECISIONS:
	- StartSession auto-detects raid name from GetInstanceInfo() if not provided
	- Pending → Core promotion happens on UpdateRoster (attendance-based)
	- Boss EP amounts are hardcoded per difficulty (not configurable)
	- OnPointChange creates roster entries for standby/late joins (permissive)
	
	SECURITY MODEL:
	StartSession and EndSession require IsOfficer() verification (caller enforced).
	UpdateRoster is safe to call by anyone (read-only roster snapshot).
	RecordBossKill should be called by authorized sources (Session, RCLC_Bridge).
	
	EXTERNAL DEPENDENCIES:
	- Modules/EPGP.lua (MassAwardEP)
	- Modules/Communication.lua (BroadcastSessionStart/End, optional)
	- Database.lua (GetPlayerData, GenerateSessionID)
	- Utilities.lua (GetRaidRoster, IsPlayerInRaid, CopyTable)
	- WoW API: GetInstanceInfo, GetNumGroupMembers, UnitName, UnitClass
	
	TEST COVERAGE:
	- TEST-SESSION-001: State machine, RBAC, sessionID generation, archival
	- TEST-ROSTER-001: Roster snapshots, promotion logic, point tracking
]]--

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

-- State
Session.onTimeTimer = nil  -- Timer handle for grace period
Session.progressionTimer = nil  -- Timer handle for 30-min EP
Session.progressionCount = 0  -- Track ticks for summary

--[[=============================================
	Session Management
===============================================]]

function Session:StartSession(raidName)
	if not Cerebro:IsOfficer() then
		Cerebro:Print("Only officers can start a raid session.")
		return
	end

	if self:IsActive() then
		Cerebro:Print("A raid session is already active. End it first.")
		return
	end

	-- Auto-detect raid name
	if not raidName then
		local name, instanceType = GetInstanceInfo()
		if instanceType == "raid" then
			raidName = name
		else
			raidName = "Raid Night"
		end
	end

	-- Create new session
	local sessionID = Cerebro:GenerateSessionID(raidName)

	Cerebro.db.global.currentSession = {
		sessionID = sessionID,
		raidName = raidName,
		startTime = time(),
		endTime = nil,
		roster = {},
		bosses = {},
	}

	self:UpdateRoster()

	Cerebro:Print("Raid session started: " .. raidName)
	Cerebro:SendRaidMessage("[Cerebro] EP/GP Session Started.")
	
	-- Add to History tab (as special transaction)
	if Cerebro.Ledger and Cerebro.Ledger.AddTransaction then
		Cerebro.Ledger:AddTransaction(
			"SESSION", -- player field (used for filtering, set to "SESSION")
			0,         -- amount (zero for informational entry)
			"EP",      -- currency
			"SESSION_START", -- reason
			{ sessionInfo = string.format("[%s_%s] Session Started.", raidName, sessionID) },
			false,     -- not import
			{ silent = false } -- broadcast this
		)
	end
	
	-- Add to System Logs tab
	if Cerebro.Ledger and Cerebro.Ledger.AddAuditLog then
		Cerebro.Ledger:AddAuditLog("SESSION_START", 
			string.format("[%s_%s] Session Started.", raidName, sessionID))
	end

	if Cerebro.modules.Communication then
		Cerebro.modules.Communication:BroadcastSessionStart(sessionID, raidName)
	end
	
	-- Schedule on-time bonus timer
	local gracePeriod = Cerebro.db.global.config.epgp.onTimeGracePeriod or 5
	if gracePeriod > 0 then
		self.onTimeTimer = self:ScheduleTimer("OnGraceExpiry", gracePeriod * 60)
		Cerebro:Debug(string.format("On-time timer scheduled for %d minutes", gracePeriod))
	else
		-- Award immediately if grace period is 0
		self:OnGraceExpiry()
	end
	
	-- Schedule 30-minute progression EP timer
	local epProgression = Cerebro.db.global.config.epgp.epProgressionHourly or 50
	if epProgression > 0 then
		self.progressionTimer = self:ScheduleRepeatingTimer("AwardProgressionEP", 1800) -- 30 min
		self.progressionCount = 0
		Cerebro:Debug("30-minute progression timer started")
	end

	return sessionID
end

--[[=============================================
	On-Time Bonus
===============================================]]

function Session:OnGraceExpiry()
	-- Guard: session may have ended before timer fires
	if not self:IsActive() then
		Cerebro:Debug("OnGraceExpiry: Session not active, skipping")
		return
	end
	
	local session = self:GetCurrentSession()
	if not session then return end
	
	-- Snapshot current roster as on-time
	self:UpdateRoster()
	local onTimeCount = 0
	local epOnTime = Cerebro.db.global.config.epgp.epOnTime or 50
	
	for player, entry in pairs(session.roster) do
		if entry.onTime == nil then
			-- Mark everyone currently in roster as on-time
			entry.onTime = true
			onTimeCount = onTimeCount + 1
			Cerebro:Debug("OnGraceExpiry: Marked " .. player .. " as on-time")
		end
	end
	Cerebro:Debug("OnGraceExpiry: Total on-time count = " .. onTimeCount)
	
	-- Award on-time EP to Core players who are on-time
	if epOnTime > 0 and onTimeCount > 0 then
		Cerebro:GetModule("EPGP"):MassAwardEP(epOnTime, "ON_TIME", function(name, data)
			local rosterEntry = session.roster[name]
			return rosterEntry and rosterEntry.onTime and data and data.status == "Core"
		end)
		
		-- Update attendance.onTime counter for each player
		for player, entry in pairs(session.roster) do
			if entry.onTime then
				local playerData = Cerebro:GetPlayerData(player)
				if playerData and playerData.attendance and playerData.status == "Core" then
					playerData.attendance.onTime = (playerData.attendance.onTime or 0) + 1
				end
			end
		end
		
		Cerebro:SendRaidMessage(string.format("[Cerebro] On-Time bonus awarded to %d players.", onTimeCount))
	end
	
	self.onTimeTimer = nil
end

function Session:EndSession()
	if not Cerebro:IsOfficer() then return end

	local session = self:GetCurrentSession()
	if not session then return end
	
	-- Cancel on-time timer if still pending
	if self.onTimeTimer then
		self:CancelTimer(self.onTimeTimer)
		self.onTimeTimer = nil
	end
	
	-- Cancel progression timer if still running
	if self.progressionTimer then
		self:CancelTimer(self.progressionTimer)
		self.progressionTimer = nil
	end

	-- Award Full Attendance bonus
	self:AwardFullAttendance(session)

	session.endTime = time()
	
	-- Update attendance.total for all participants
	for player, entry in pairs(session.roster) do
		local playerData = Cerebro:GetPlayerData(player)
		if playerData and playerData.attendance and playerData.status == "Core" then
			playerData.attendance.total = (playerData.attendance.total or 0) + 1
		end
	end
	
	-- Print session summaries
	self:PrintRaidSummary(session)
	self:ScheduleTimer("PrintPersonalSummary", 3)  -- 3-second delay
	
	-- Debug: Dump roster flags BEFORE copy
	local myName = Cerebro:GetFullPlayerName(UnitName("player"))
	Cerebro:Debug("EndSession: Before copy - checking roster for " .. myName)
	for player, entry in pairs(session.roster) do
		Cerebro:Debug(string.format("  %s: onTime=%s fullAttendance=%s sessionEP=%s",
			player, tostring(entry.onTime), tostring(entry.fullAttendance), tostring(entry.sessionEP)))
	end
	
	-- Move to history
	Cerebro.db.global.history[session.sessionID] = Cerebro:CopyTable(session)
	Cerebro.db.global.currentSession = nil

	Cerebro:Print("Raid session ended.")
	Cerebro:SendRaidMessage("[Cerebro] Session Ended.")
	
	-- Add to History tab (as special transaction)
	if Cerebro.Ledger and Cerebro.Ledger.AddTransaction then
		Cerebro.Ledger:AddTransaction(
			"SESSION", -- player field
			0,         -- amount (zero for informational entry)
			"EP",      -- currency
			"SESSION_END", -- reason
			{ sessionInfo = string.format("[%s_%s] Session Ended. (Duration: %dm)", 
				session.raidName or "?", session.sessionID,
				math.floor((session.endTime - session.startTime) / 60)) },
			false,     -- not import
			{ silent = false } -- broadcast this
		)
	end
	
	-- Add to System Logs tab
	if Cerebro.Ledger and Cerebro.Ledger.AddAuditLog then
		Cerebro.Ledger:AddAuditLog("SESSION_END", 
			string.format("[%s_%s] Session Ended. (Duration: %dm)", 
				session.raidName or "?", session.sessionID,
				math.floor((session.endTime - session.startTime) / 60)))
	end

	if Cerebro.modules.Communication then
		Cerebro.modules.Communication:BroadcastSessionEnd(session.sessionID)
		-- Sync data after session ends
		Cerebro.modules.Communication:SyncToRaid()
	end
end

--[[=============================================
	Full Attendance Award
===============================================]]

function Session:AwardFullAttendance(session)
	local epFullAttendance = Cerebro.db.global.config.epgp.epFullAttendance or 100
	if epFullAttendance <= 0 then return end
	
	local bossCount = #session.bosses
	local currentRoster = Cerebro:GetRaidRoster()
	local currentRosterNames = {}
	for _, member in ipairs(currentRoster) do
		currentRosterNames[member.name] = true
	end
	
	local qualifyingPlayers = 0
	
	-- Full attendance requires:
	-- 1. onTime = true
	-- 2. Present at end (in current raid roster)
	-- 3. If bosses killed: 100% boss attendance
	for player, entry in pairs(session.roster) do
		local playerData = Cerebro:GetPlayerData(player)
		local status = playerData and playerData.status or "NONE"
		local isOnTime = entry.onTime == true
		local isPresentNow = currentRosterNames[player] == true
		local fullBossAttendance = (bossCount == 0) or ((entry.bossesPresent or 0) >= bossCount)
		
		Cerebro:Debug(string.format("FullAttendance check: %s status=%s onTime=%s present=%s bossOk=%s", 
			player, status, tostring(isOnTime), tostring(isPresentNow), tostring(fullBossAttendance)))
		
		if playerData and playerData.status == "Core" then
			if isOnTime and isPresentNow and fullBossAttendance then
				entry.fullAttendance = true
				qualifyingPlayers = qualifyingPlayers + 1
				Cerebro:Debug("  -> QUALIFIED for full attendance")
			else
				Cerebro:Debug("  -> NOT qualified")
			end
		else
			Cerebro:Debug("  -> SKIPPED (not Core)")
		end
	end
	
	if qualifyingPlayers > 0 then
		Cerebro:GetModule("EPGP"):MassAwardEP(epFullAttendance, "FULL_ATTENDANCE", function(name, data)
			local rosterEntry = session.roster[name]
			return rosterEntry and rosterEntry.fullAttendance and data and data.status == "Core"
		end)
		
		-- Update attendance.full counter
		for player, entry in pairs(session.roster) do
			if entry.fullAttendance then
				local playerData = Cerebro:GetPlayerData(player)
				if playerData and playerData.attendance then
					playerData.attendance.full = (playerData.attendance.full or 0) + 1
				end
			end
		end
		
		Cerebro:SendRaidMessage(string.format("[Cerebro] Full Attendance bonus awarded to %d players.", qualifyingPlayers))
	end
end

function Session:IsActive()
	return Cerebro.db.global.currentSession ~= nil
end

function Session:GetCurrentSession()
	return Cerebro.db.global.currentSession
end

--[[=============================================
	Roster Management
===============================================]]

function Session:UpdateRoster()
	local session = self:GetCurrentSession()
	if not session then return end

	local currentTime = time()
	local gracePeriod = Cerebro.db.global.config.epgp.onTimeGracePeriod or 5
	local graceEndTime = session.startTime + (gracePeriod * 60)
	local roster = Cerebro:GetRaidRoster()

	for _, member in ipairs(roster) do
		local player = member.name
		if not session.roster[player] then
			-- Determine if this is a late join (after grace period)
			local isLate = currentTime > graceEndTime and self.onTimeTimer == nil
			
			session.roster[player] = {
				joinTime = currentTime,
				class = member.class,
				sessionEP = 0,
				sessionGP = 0,
				onTime = isLate and false or nil,  -- nil = pending grace check, false = late
				bossesPresent = 0,
			}
			local playerData = Cerebro:GetPlayerData(player)
            playerData.class = member.class
            
            -- Bug Fix: Removed auto-promotion from Pending to Core
            -- Raiders stay Pending until officers explicitly admit them via Officer Tools
		end
	end
end

function Session:OnPointChange(player, amount, type)
	local session = self:GetCurrentSession()
	if not session then return end
	
	-- Ensure player in roster
	if not session.roster[player] then
		-- If they aren't in roster (e.g. standby), add them purely for tracking?
		-- Or ignore. Let's add them to track standby gains.
		session.roster[player] = {
			joinTime = time(),
			class = "Unknown", -- Will be fixed if they zone in or we query
			sessionEP = 0,
			sessionGP = 0
		}
	end
	
	if type == "EP" then
		session.roster[player].sessionEP = (session.roster[player].sessionEP or 0) + amount
	elseif type == "GP" then
		session.roster[player].sessionGP = (session.roster[player].sessionGP or 0) + amount
	end
end

-- Helper for UI
function Session:GetSessionStats(player)
	local session = self:GetCurrentSession()
	if not session or not session.roster[player] then
		return 0, 0
	end
	return session.roster[player].sessionEP or 0, session.roster[player].sessionGP or 0
end

--[[=============================================
	Boss Tracking
===============================================]]

function Session:RecordBossKill(encounterID, encounterName, difficultyID)
	local session = self:GetCurrentSession()
	if not session then return end

	for _, boss in ipairs(session.bosses) do
		if boss.encounterID == encounterID and boss.killed then
			return
		end
	end
	
	-- Snapshot attendees before creating boss record
	self:UpdateRoster()
	local attendees = {}
	for player, entry in pairs(session.roster) do
		if Cerebro:IsPlayerInRaid(player) then
			table.insert(attendees, player)
			-- Increment per-player boss attendance
			entry.bossesPresent = (entry.bossesPresent or 0) + 1
			
			-- Increment global player boss kill counter
			local playerData = Cerebro:GetPlayerData(player)
			if playerData and playerData.status == "Core" then
				playerData.bossKillsPresent = (playerData.bossKillsPresent or 0) + 1
			end
		end
	end

	local bossData = {
		encounterID = encounterID,
		name = encounterName,
		difficultyID = difficultyID,
		killed = true,
		killTime = time(),
		attendees = attendees,  -- Who was present for this kill
	}

	table.insert(session.bosses, bossData)
	Cerebro.db.global.totalBossKills = (Cerebro.db.global.totalBossKills or 0) + 1
	Cerebro.db.global.seasonBossKills = (Cerebro.db.global.seasonBossKills or 0) + 1
	
	-- Award EP for Boss
	-- Calculate amount based on diff
	local ep = 50 -- Default
	if difficultyID == 15 then ep = 75 end -- Heroic
	if difficultyID == 16 then ep = 100 end -- Mythic
	
	-- FIX: Only award EP to Core guild members who are in the raid (not pugs)
	Cerebro:GetModule("EPGP"):MassAwardEP(ep, "BOSS: " .. encounterName, function(name, data)
		return Cerebro:IsPlayerInRaid(name) and data and data.status == "Core"
	end)
	
	Cerebro:SendRaidMessage(string.format("[Cerebro] Boss %s down! Awarded %d EP.", encounterName, ep))
end

--[[=============================================
	Progression EP (30-minute)
===============================================]]

function Session:AwardProgressionEP()
	if not self:IsActive() then return end
	
	local session = self:GetCurrentSession()
	if not session then return end
	
	local ep = Cerebro.db.global.config.epgp.epProgressionHourly or 50
	if ep <= 0 then return end
	
	self.progressionCount = (self.progressionCount or 0) + 1
	self:UpdateRoster()
	
	Cerebro:GetModule("EPGP"):MassAwardEP(ep, "PROGRESSION", function(name, data)
		return Cerebro:IsPlayerInRaid(name) and data and data.status == "Core"
	end)
	
	Cerebro:SendRaidMessage(string.format("[Cerebro] Progression EP awarded: %d EP", ep))
end

--[[=============================================
	Session Summary
===============================================]]

function Session:PrintRaidSummary(session)
	local cfg = Cerebro.db.global.config.epgp
	local bossCount = #session.bosses
	local duration = (session.endTime or time()) - session.startTime
	local hours = math.floor(duration / 3600)
	local mins = math.floor((duration % 3600) / 60)
	
	-- Calculate max obtainable EP
	local epOnTime = cfg.epOnTime or 50
	local epProgression = cfg.epProgressionHourly or 50
	local epFullAttendance = cfg.epFullAttendance or 100
	local progressionTicks = self.progressionCount or 0
	
	-- Boss EP (difficulty-based)
	local bossEP = 0
	for _, boss in ipairs(session.bosses) do
		local ep = 50 -- Normal
		if boss.difficultyID == 15 then ep = 75 end -- Heroic
		if boss.difficultyID == 16 then ep = 100 end -- Mythic
		bossEP = bossEP + ep
	end
	
	local maxEP = epOnTime + (epProgression * progressionTicks) + bossEP + epFullAttendance
	
	Cerebro:SendRaidMessage("----------------------------------")
	Cerebro:SendRaidMessage("SESSION SUMMARY")
	Cerebro:SendRaidMessage(string.format("  Duration: %dh %dm - Bosses: %d", hours, mins, bossCount))
	Cerebro:SendRaidMessage("  ---------------------------")
	Cerebro:SendRaidMessage(string.format("  On-Time Bonus: %d EP", epOnTime))
	Cerebro:SendRaidMessage(string.format("  Progression (%dx): %d EP", progressionTicks, epProgression * progressionTicks))
	Cerebro:SendRaidMessage(string.format("  Boss Kills (%dx): %d EP", bossCount, bossEP))
	Cerebro:SendRaidMessage(string.format("  Full Attendance: %d EP", epFullAttendance))
	Cerebro:SendRaidMessage("  ---------------------------")
	Cerebro:SendRaidMessage(string.format("  Max Obtainable: %d EP (Raid EP, not Readiness)", maxEP))
	Cerebro:SendRaidMessage("----------------------------------")
end

function Session:PrintPersonalSummary()
	-- Get session from history (already archived at this point)
	local session = nil
	local latestTime = 0
	for sessionID, s in pairs(Cerebro.db.global.history) do
		if s.endTime and s.endTime > latestTime then
			latestTime = s.endTime
			session = s
			Cerebro:Debug("PrintPersonalSummary: Candidate session " .. sessionID .. " endTime=" .. s.endTime)
		end
	end
	if not session then 
		Cerebro:Debug("PrintPersonalSummary: No session found in history!")
		return 
	end
	Cerebro:Debug("PrintPersonalSummary: Using session " .. (session.sessionID or "?"))
	
	local cfg = Cerebro.db.global.config.epgp
	local playerName = Cerebro:GetFullPlayerName(UnitName("player"))
	Cerebro:Debug("PrintPersonalSummary: Looking up player " .. playerName)
	
	-- Debug: List all roster keys
	for rosterName, _ in pairs(session.roster or {}) do
		Cerebro:Debug("  Roster entry: " .. rosterName)
	end
	
	local entry = session.roster[playerName]
	if not entry then 
		Cerebro:Debug("PrintPersonalSummary: Player NOT found in roster!")
		return 
	end
	Cerebro:Debug(string.format("PrintPersonalSummary: Found entry - onTime=%s fullAttendance=%s sessionEP=%s",
		tostring(entry.onTime), tostring(entry.fullAttendance), tostring(entry.sessionEP)))
	
	local epOnTime = cfg.epOnTime or 50
	local epProgression = cfg.epProgressionHourly or 50
	local epFullAttendance = cfg.epFullAttendance or 100
	
	-- Calculate what this player earned
	local earnedOnTime = entry.onTime and epOnTime or 0
	local earnedFullAttendance = entry.fullAttendance and epFullAttendance or 0
	local earnedProgression = (entry.progressionTicks or self.progressionCount or 0) * epProgression
	
	-- Calculate boss EP from session tracking
	local earnedBoss = (entry.sessionEP or 0) - earnedOnTime - earnedProgression - earnedFullAttendance
	if earnedBoss < 0 then earnedBoss = 0 end
	
	local total = entry.sessionEP or 0
	
	Cerebro:Print("Your Session Summary:")
	Cerebro:Print(string.format("  On-Time Bonus: %s", entry.onTime and ("+" .. epOnTime .. " EP") or "MISSED"))
	Cerebro:Print(string.format("  Progression: +%d EP", earnedProgression))
	Cerebro:Print(string.format("  Boss Kills: +%d EP", earnedBoss))
	Cerebro:Print(string.format("  Full Attendance: %s", entry.fullAttendance and ("+" .. epFullAttendance .. " EP") or "MISSED"))
	Cerebro:Print("  ---------------------------")
	Cerebro:Print(string.format("  Your Total: +%d EP (Raid EP, not Readiness)", total))
end
