--[[
	Cerebro - Sync Scheduler
	
	PURPOSE:
	Centralises all sync-timing decisions into a single module.
	Owns encounter state, the 30-second heartbeat timer, and the
	post-combat flush logic.  Nothing in this module touches payload
	construction or data merging — that stays in Communication.lua.
	
	RESPONSIBILITIES:
	1. ShouldSync() — unified gate used by Communication and callers
	2. Encounter lifecycle (ENCOUNTER_START / ENCOUNTER_END)
	3. Post-combat flush via PLAYER_REGEN_ENABLED
	4. Heartbeat timer (30 s repeating sync trigger)
	5. Boss-kill forwarding to Session module (officer only)
	
	CONTRACTS:
	- ShouldSync()   -> boolean  (safe to broadcast right now?)
	- GetSyncStatus() -> string  (human-readable reason if blocked)
	
	DESIGN DECISIONS:
	- Direct call to Session:RecordBossKill (Option A) keeps it simple.
	- 5-second safety buffer after ENCOUNTER_END per WoW 12.0 research
	  (see knowledge: cerebro_technical_constraints / combat_restrictions_research)
	- 2-second delay on PLAYER_REGEN_ENABLED flush to let lockdown fully clear.
]]--

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

--[[=============================================
	State
===============================================]]

SyncScheduler.encounterState = {
	inEncounter = false,
	startedAt = 0,
	endedAt = 0,
	pendingSyncFlush = false,
}

-- Sync Leader state (Fix 2)
SyncScheduler._cachedLeader = nil
SyncScheduler._cachedLeaderAt = 0

--[[=============================================
	Lifecycle
===============================================]]

function SyncScheduler:OnEnable()
	self:RegisterEvent("ENCOUNTER_START")
	self:RegisterEvent("ENCOUNTER_END")
	self:RegisterEvent("PLAYER_REGEN_ENABLED")
	-- Raid Sync v2: Read heartbeat interval from config (default 30s)
	local s = Cerebro.db.global.syncState or {}
	local cfg = s.raidSyncConfig or {}
	local interval = cfg.heartbeatInterval or 30
	self:ScheduleRepeatingTimer("OnHeartbeat", interval)
end

--[[=============================================
	Unified Sync Gate
	Replaces scattered IsEncounterActive() + InCombatLockdown() checks
===============================================]]

-- Returns true if it is safe to broadcast sync data right now.
function SyncScheduler:ShouldSync()
	if self.encounterState.inEncounter then
		return false
	end
	if InCombatLockdown() then
		return false
	end
	return true
end

-- Returns a human-readable reason when sync is blocked, nil when clear.
function SyncScheduler:GetSyncBlockReason()
	if self.encounterState.inEncounter then
		return "encounter active"
	end
	if InCombatLockdown() then
		return "combat lockdown"
	end
	return nil
end

--[[=============================================
	Event Handlers  (moved from Core.lua)
===============================================]]

function SyncScheduler:ENCOUNTER_START(_event, encounterID, encounterName, _difficultyID, _groupSize)
	Cerebro:Debug("Encounter started: " .. encounterName .. " (ID: " .. encounterID .. ")")

	-- Suppress syncs during encounter
	self.encounterState.inEncounter = true
	self.encounterState.startedAt = GetTime()
	Cerebro:Debug("Combat sync suppressed (encounter started)")

	-- Notify other modules (e.g. Session bidding lock)
	if Cerebro.modules.Session and Cerebro.modules.Session:IsActive() then
		Cerebro:SendMessage("ENCOUNTER_START", encounterID, encounterName)
	end
end

function SyncScheduler:ENCOUNTER_END(_event, encounterID, encounterName, difficultyID, _groupSize, success)
	Cerebro:Debug("Encounter ended: " .. encounterName .. " (Success: " .. tostring(success) .. ")")

	-- 5-second safety buffer: combat lockdown persists briefly after ENCOUNTER_END
	Cerebro:ScheduleTimer(function()
		self.encounterState.inEncounter = false
		self.encounterState.endedAt = GetTime()
		self.encounterState.pendingSyncFlush = true
		Cerebro:Debug("Encounter state cleared, sync flush pending")
	end, 5)

	-- Boss-kill forwarding (Option A: direct call)
	if success and Cerebro.modules.Session and Cerebro.modules.Session:IsActive() then
		-- Only officers process boss kills to prevent non-officers from awarding EP
		if Cerebro:IsOfficer() then
			Cerebro.modules.Session:RecordBossKill(encounterID, encounterName, difficultyID)
			Cerebro:SendMessage("BOSS_KILL", encounterID, encounterName)
		end
	end
end

-- Post-combat flush: broadcast pending data once lockdown is fully clear
function SyncScheduler:PLAYER_REGEN_ENABLED()
	-- 2-second delay to ensure lockdown is fully clear
	Cerebro:ScheduleTimer(function()
		if self.encounterState.pendingSyncFlush then
			self.encounterState.pendingSyncFlush = false
			Cerebro:Debug("Flushing pending sync after combat")
			if Cerebro:IsOfficer() and Cerebro.modules.CommunicationV2 then
				Cerebro.modules.CommunicationV2:SyncToRaid()
			end
		end
	end, 2)
end

--[[=============================================
	Sync Mode (Raid Sync v2)
	Rich 4-state machine for sync decisions.
	ShouldSync() remains as a simple boolean gate for callers
	that don't need the full state.
===============================================]]

-- Returns the current sync mode: "OFF" | "RAID_IDLE" | "RAID_COMBAT" | "RAID_POST"
function SyncScheduler:GetSyncMode()
	if not IsInRaid() then return "OFF" end
	if self.encounterState.inEncounter then return "RAID_COMBAT" end
	if InCombatLockdown() then return "RAID_COMBAT" end
	if self.encounterState.pendingSyncFlush then return "RAID_POST" end
	return "RAID_IDLE"
end

--[[=============================================
	Heartbeat Timer  (moved from Core.lua)
	Raid Sync v2: Smart heartbeat with peer-aware decisions
===============================================]]

function SyncScheduler:OnHeartbeat()
	if not Cerebro:IsOfficer() then return end

	-- Quarantine guard: suppress heartbeats during factory reset window
	-- (prevents broadcasting empty/zeroed hashes that trigger false sync)
	if Cerebro.modules.Communication and Cerebro.modules.Communication:IsQuarantined() then
		return
	end

	local mode = self:GetSyncMode()

	-- V2-only: ALL officers broadcast v2 heartbeats (lightweight hash-only).
	-- Combat-gated: respect encounter state.
	if mode ~= "RAID_COMBAT" and Cerebro.modules.CommunicationV2 then
		Cerebro.modules.CommunicationV2:SendHeartbeat()
	end

	-- Post-combat flush: run sync immediately after encounter ends
	if mode == "RAID_POST" then
		self:MaybeRaidSync(mode)
	end
end

-- Smart sync decision: V2-only, uses hash-based sync
function SyncScheduler:MaybeRaidSync(mode)
	-- RAID_POST: always flush (post-combat window)
	if mode == "RAID_POST" then
		Cerebro:Debug("MaybeRaidSync: RAID_POST flush")
		if Cerebro.modules.CommunicationV2 then
			Cerebro.modules.CommunicationV2:SyncToRaid()
		end
		return
	end

	-- RAID_IDLE: V2 heartbeat handles divergence detection automatically
	-- No need for periodic full broadcasts or stale peer checks — V2 heartbeat
	-- comparison triggers STATE_FETCH/STATE_SNAPSHOT only when hashes diverge
	Cerebro:Debug("MaybeRaidSync: V2 heartbeat handles sync, skipping")
end

--[[=============================================
	Sync Leader Election (Fix 2)
	Only the highest-ranking eligible online officer in the raid
	broadcasts heartbeat data. Ties broken by alphabetical name.
	Other officers still RECEIVE and MERGE data, but don't broadcast.
===============================================]]

--- Determines if the current player is the Sync Leader.
-- Uses a 5-second cache to avoid rescanning the raid roster every heartbeat.
-- Outside of raid, always returns true (all officers can sync normally).
function SyncScheduler:IsSyncLeader()
	if not IsInRaid() then return true end
	if not Cerebro:IsOfficer() then return false end

	-- Use cached result if recent (5-second TTL)
	local now = GetTime()
	if self._cachedLeader and (now - self._cachedLeaderAt) < 5 then
		return self._cachedLeader == Cerebro:GetPlayerFullName()
	end

	-- Elect: scan raid roster for the highest-ranking eligible officer
	local leader = self:ElectSyncLeader()
	self._cachedLeader = leader
	self._cachedLeaderAt = now

	return leader == Cerebro:GetPlayerFullName()
end

--- Returns the full name of the current Sync Leader.
-- Used by Fix 2c for EPGP forwarding.
function SyncScheduler:GetSyncLeaderName()
	if not IsInRaid() then return nil end

	-- Refresh cache if stale
	local now = GetTime()
	if not self._cachedLeader or (now - self._cachedLeaderAt) >= 5 then
		self._cachedLeader = self:ElectSyncLeader()
		self._cachedLeaderAt = now
	end

	return self._cachedLeader
end

--- Internal: Elect the sync leader from raid members.
-- Scans raid roster, filters for officers with eligible guild ranks,
-- returns the one with lowest rankIndex (ties broken alphabetically).
function SyncScheduler:ElectSyncLeader()
	-- Fix 5: Read from config.sync (merged location) instead of syncState.raidSyncConfig
	local syncConfig = (Cerebro.db.global.config.sync or {})
	local eligibleSet = syncConfig.syncLeaderRanks or { [0] = true, [1] = true }

	-- Scan raid roster for eligible officers
	local candidates = {}
	for i = 1, GetNumGroupMembers() do
		local name, _, _, _, _, _, _, online = GetRaidRosterInfo(i)
		if name and online then
			local fullName = Cerebro:GetFullPlayerName(name)
			if Cerebro:IsOfficer(name) then
				-- Use the cached rank index lookup (Fix 5)
				local rankIndex = Cerebro:GetPlayerGuildRankIndex(fullName)

				-- Only add if rank is eligible (or we can't determine rank — fallback)
				if rankIndex and eligibleSet[rankIndex] then
					table.insert(candidates, { name = fullName, rankIndex = rankIndex })
				elseif not rankIndex then
					-- Can't determine rank — include with high index so they're last resort
					table.insert(candidates, { name = fullName, rankIndex = 999 })
				end
			end
		end
	end

	-- Sort: lowest rank first, then alphabetical
	table.sort(candidates, function(a, b)
		if a.rankIndex ~= b.rankIndex then
			return a.rankIndex < b.rankIndex
		end
		return a.name < b.name
	end)

	if #candidates > 0 then
		return candidates[1].name
	end

	-- Fallback: if no eligible candidates found, current player is leader
	-- (shouldn't happen if the player is an officer in raid)
	return Cerebro:GetPlayerFullName()
end
