## RoundManager — Match lifecycle singleton.
##
## Orchestrates warmup → live rounds → round end → next round.
## Owned by GameServer; wired to TeamManager + DamageProcessor.
##
## Architecture:
##   GameServer
##   ├── RoundManager (child) — round lifecycle, timers, phase tracking
##   ├── SimulationServer (C++) — 128Hz tick loop
##   ├── WeaponServer / LagCompensation / DamageProcessor
##   └── (TeamManager / SpawnManager live outside but are referenced)
##
## Usage (inside GameServer._ready()):
##   var rm = RoundManager.new()
##   add_child(rm)
##   rm.team_manager = team_manager_ref
##   rm.damage_processor = damage_processor_ref
##
## Signals connect round events to economy, HUD, scoreboard, etc.

extends Node
class_name RoundManager
const _td_rm = preload("res://scripts/teams/team_data.gd")

# ---------------------------------------------------------------------------
# Round Phase enum
# ---------------------------------------------------------------------------
enum RoundPhase {
	WARMUP = 0,  # Pre-match free-move, no scoring, no economy
	PREP   = 1,  # Freezetime / buy phase
	LIVE   = 2,  # Round active
	POST   = 3,  # Post-round scoreboard + transition
}

# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------

## Emitted whenever the round phase transitions.
## phase: the new RoundPhase value (int for cross-script compatibility).
## round_num: the current round number.
signal round_phase_changed(phase: int, round_num: int)

## Emitted when a PREP round begins.
signal round_started(round_num: int)

## Emitted when a LIVE round ends.
## winning_team: the Team enum of the winning team.
## reason: string describing the reason (e.g. "elimination", "time_expired").
signal round_ended(winning_team: int, reason: String)

## Emitted when one team reaches match-win threshold.
## team: the Team enum of the team at match point.
signal match_point(team: int)

## Emitted when warmup ends and the server is ready for competitive play.
signal warmup_ended()

## Emitted to relay round end economy data to the economy system.
## winning_team: which team won the round.
## team_money: Dictionary mapping team enum → {win: int, loss: int}
signal round_economy_data(winning_team: int, team_money: Dictionary)

# ---------------------------------------------------------------------------
# Constants — defaults
# ---------------------------------------------------------------------------

## Default PREP (freeze-time + buy) duration in seconds.
const DEFAULT_PREP_DURATION: float = 15.0

## Default LIVE round duration in seconds.
const DEFAULT_LIVE_DURATION: float = 105.0

## Default POST (scoreboard) duration in seconds.
const DEFAULT_POST_DURATION: float = 8.0

## Default win money awarded to each player on the winning team.
const DEFAULT_WIN_MONEY: int = 3250

## Default base loss money.
const DEFAULT_LOSS_MONEY: int = 1900

## Default max loss money after consecutive losses.
const DEFAULT_LOSS_MONEY_MAX: int = 2900

## Default kill bonus awarded to the killer at round end.
const DEFAULT_KILL_BONUS: int = 300

## Default number of round wins required to win the match.
const DEFAULT_WIN_THRESHOLD: int = 16

# ---------------------------------------------------------------------------
# Dependencies (set by GameServer before use)
# ---------------------------------------------------------------------------

## Reference to the TeamManager node/singleton.
var team_manager: TeamManager = null

## Reference to the DamageProcessor node/singleton.
var damage_processor: DamageProcessor = null

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

## Duration of the PREP (freeze-time + buy) phase in seconds.
var prep_duration: float = DEFAULT_PREP_DURATION

## Duration of the LIVE round phase in seconds.
var live_duration: float = DEFAULT_LIVE_DURATION

## Duration of the POST round (scoreboard) phase in seconds.
var post_duration: float = DEFAULT_POST_DURATION

## Money awarded to each winning player at round end.
var win_money: int = DEFAULT_WIN_MONEY

## Base money awarded to each losing player at round end.
var loss_money_base: int = DEFAULT_LOSS_MONEY

## Maximum loss money after consecutive losses.
var loss_money_max: int = DEFAULT_LOSS_MONEY_MAX

## Kill bonus per kill at round end.
var kill_bonus: int = DEFAULT_KILL_BONUS

## Rounds needed to win the match (first to this many).
var win_threshold: int = DEFAULT_WIN_THRESHOLD

# ---------------------------------------------------------------------------
# State
# ---------------------------------------------------------------------------

## Current round phase.
var _phase: RoundPhase = RoundPhase.WARMUP

## Current round number (1-indexed, increments each round).
var _round_num: int = 0

## Timer tracking remaining seconds in the current phase.
var _phase_timer: float = 0.0

## Whether the match has started (exited warmup).
var _match_started: bool = false

## Consecutive losses per team (team enum → int).
## Used for loss-money escalation.
var _consecutive_losses: Dictionary = {}  # Team enum → int

## Track kills per shooter per round (for round-end kill bonus).
## shooter_id (int) → kill_count (int) for the current round.
var _round_kills: Dictionary = {}

## Track deaths per entity per round (for stats).
var _round_deaths: Dictionary = {}  # entity_id (int) → death_count (int)

# ---------------------------------------------------------------------------
# Lifecycle
# ---------------------------------------------------------------------------

func _ready() -> void:
	start_warmup()

func _process(delta: float) -> void:
	_advance_timer(delta)

# ---------------------------------------------------------------------------
# Initialization
# ---------------------------------------------------------------------------

## Start the server in WARMUP mode — players can move/shoot,
## no scoring, no economy. Called automatically on _ready().
func start_warmup() -> void:
	_phase = RoundPhase.WARMUP
	_phase_timer = INF  # Runs forever until admin starts match or enough players join
	_round_num = 0
	_match_started = false
	_round_kills.clear()
	_round_deaths.clear()
	_consecutive_losses = {
		_td_rm.Team.COUNTER_TERRORIST: 0,
		_td_rm.Team.TERRORIST: 0,
	}
	print("[RoundManager] Warmup started — infinite duration")
	round_phase_changed.emit(RoundPhase.WARMUP, 0)

# ---------------------------------------------------------------------------
# Match / Round Control
# ---------------------------------------------------------------------------

## Begin a competitive match. Transitions from WARMUP to PREP for round 1.
## Called automatically when enough players join, or via RCON `start_match`.
func start_match() -> void:
	if _phase != RoundPhase.WARMUP:
		push_warning("[RoundManager] start_match called but not in WARMUP (phase=%d)" % _phase)
		return

	print("[RoundManager] Match starting!")
	warmup_ended.emit()
	start_round(1)

## Start a new round at the given round number.
## Transitions to PREP phase (freezetime + buy phase).
func start_round(round_num: int) -> void:
	if not _match_started:
		_match_started = true

	_round_num = round_num
	_phase = RoundPhase.PREP
	_phase_timer = prep_duration

	# Reset round-specific tracking
	_round_kills.clear()
	_round_deaths.clear()

	print("[RoundManager] Round %d started — PREP phase (% .1fs freezetime)" % [_round_num, _phase_timer])
	round_phase_changed.emit(RoundPhase.PREP, _round_num)
	round_started.emit(_round_num)

## Transition from PREP to LIVE.
## Called automatically when the PREP timer expires.
func start_live() -> void:
	if _phase != RoundPhase.PREP:
		push_warning("[RoundManager] start_live called but not in PREP (phase=%d)" % _phase)
		return

	_phase = RoundPhase.LIVE
	_phase_timer = live_duration

	print("[RoundManager] Round %d is LIVE — % .1fs remaining" % [_round_num, _phase_timer])
	round_phase_changed.emit(RoundPhase.LIVE, _round_num)

## End the current round, awarding money and transitioning to POST.
## winning_team: int enum of the winner.
## reason: string describing the outcome (e.g. "elimination", "time_expired").
func end_round(winning_team: int, reason: String) -> void:
	if _phase != RoundPhase.LIVE:
		push_warning("[RoundManager] end_round called but not in LIVE (phase=%d)" % _phase)
		return

	_phase = RoundPhase.POST
	_phase_timer = post_duration

	# --- Scoring ---
	if team_manager:
		team_manager.add_team_score(winning_team, 1)

	# --- Track consecutive losses ---
	for team in [_td_rm.Team.COUNTER_TERRORIST, _td_rm.Team.TERRORIST]:
		if team == winning_team:
			_consecutive_losses[team] = 0
		else:
			_consecutive_losses[team] = _consecutive_losses.get(team, 0) + 1

	# --- Compute economy data ---
	var team_money_data: Dictionary = {}
	for team in [_td_rm.Team.COUNTER_TERRORIST, _td_rm.Team.TERRORIST]:
		var is_winner: bool = (team == winning_team)
		var money: int = 0
		if is_winner:
			money = win_money
		else:
			var loss_streak: int = _consecutive_losses.get(team, 1)
			money = min(loss_money_base + (loss_streak - 1) * 500, loss_money_max)
		team_money_data[team] = {
			"win": win_money if is_winner else 0,
			"loss": money if not is_winner else 0,
			"kill_bonus": kill_bonus,
			"total": money,
		}

	print("[RoundManager] Round %d ended — %s wins (%s)" % [_round_num, _td_rm.get_team_name(winning_team), reason])

	# --- Match point / match win check ---
	if team_manager:
		var winner_score: int = team_manager.get_team_score(winning_team)
		if winner_score >= win_threshold - 1:
			# One more round wins the match
			match_point.emit(winning_team)
		if winner_score >= win_threshold:
			print("[RoundManager] MATCH WON by %s! (Score: %d)" % [_td_rm.get_team_name(winning_team), winner_threshold_scores(winning_team)])

	# --- Emit signals ---
	round_ended.emit(winning_team, reason)
	round_economy_data.emit(winning_team, team_money_data)
	round_phase_changed.emit(RoundPhase.POST, _round_num)

# ---------------------------------------------------------------------------
# Query
# ---------------------------------------------------------------------------

## Get the current round phase as a RoundPhase enum value.
func get_phase() -> RoundPhase:
	return _phase

## Get the remaining time in seconds for the current phase.
## Returns INF for WARMUP, 0.0 if no active timer.
func get_round_time_remaining() -> float:
	if _phase == RoundPhase.WARMUP:
		return INF
	return max(0.0, _phase_timer)

## Get the current round number (1-indexed).
func get_round_num() -> int:
	return _round_num

## Check if the match has started (exited warmup).
func is_match_started() -> bool:
	return _match_started

## Get kill counts for the current round.
func get_round_kills() -> Dictionary:
	return _round_kills.duplicate()

## Get death counts for the current round.
func get_round_deaths() -> Dictionary:
	return _round_deaths.duplicate()

## Get consecutive losses for a team.
func get_consecutive_losses(team: int) -> int:
	return _consecutive_losses.get(team, 0)

# ---------------------------------------------------------------------------
# Internal — Timer / phase advancement
# ---------------------------------------------------------------------------

func _advance_timer(delta: float) -> void:
	match _phase:
		RoundPhase.WARMUP:
			# WARMUP runs indefinitely — no timer advancement
			pass

		RoundPhase.PREP:
			_phase_timer -= delta
			if _phase_timer <= 0.0:
				start_live()

		RoundPhase.LIVE:
			_phase_timer -= delta
			if _phase_timer <= 0.0:
				# Time ran out — CT wins by default (bomb not planted / hostages not rescued)
				# In CS-style, CT wins on time expire if bomb not planted.
				# For now, the team with most alive players wins, else CT.
				var time_expired_winner: int = _determine_time_expired_winner()
				end_round(time_expired_winner, "time_expired")

		RoundPhase.POST:
			_phase_timer -= delta
			if _phase_timer <= 0.0:
				# Auto-start next round
				var next_round: int = _round_num + 1
				start_round(next_round)

# ---------------------------------------------------------------------------
# Internal — Kill tracking
# ---------------------------------------------------------------------------

## Called by GameServer when a kill occurs (connected to DamageProcessor.player_killed).
func on_player_killed(victim_id: int, shooter_id: int) -> void:
	if _phase != RoundPhase.LIVE:
		return

	# Track kills for round-end bonus
	if shooter_id >= 0:
		_round_kills[shooter_id] = _round_kills.get(shooter_id, 0) + 1

	# Track deaths
	_round_deaths[victim_id] = _round_deaths.get(victim_id, 0) + 1

	# Check for team elimination
	if team_manager and _is_team_eliminated_by_kill(shooter_id, victim_id):
		var eliminated_team: int = _get_entity_team(victim_id)
		if eliminated_team >= 0:
			var winning_team: int = _get_entity_team(shooter_id)
			if winning_team >= 0:
				end_round(winning_team, "elimination")

# ---------------------------------------------------------------------------
# Internal — Team elimination detection
# ---------------------------------------------------------------------------

## Check if a kill has eliminated the victim's entire team.
func _is_team_eliminated_by_kill(_shooter_id: int, victim_id: int) -> bool:
	if not team_manager or not damage_processor:
		return false

	var victim_team: int = _get_entity_team(victim_id)
	if victim_team < 0:
		return false

	# Get all players on the victim's team
	var team_entity_ids: Array[int] = _get_entity_ids_for_team(victim_team)

	# Check if all of them are dead (health <= 0 or not registered)
	for eid in team_entity_ids:
		var health: float = damage_processor.get_health(eid)
		if health > 0.0:
			return false  # At least one teammate is alive

	return true

## Get all entity IDs belonging to a given team.
func _get_entity_ids_for_team(team: int) -> Array[int]:
	if not team_manager:
		return []

	var player_ids: Array[int] = team_manager.get_team_players(team)
	var result: Array[int] = []

	# We need a mapping from player_id → entity_id.
	# This is stored in GameServer.entity_to_peer (inverted).
	# For now, we check DamageProcessor directly if entities are tracked.
	if damage_processor:
		# We don't have a direct player→entity mapping here, so we iterate
		# the damage processor's health dictionary and infer by entity_id
		# naming convention or stored mapping.
		# Since we don't have access to entity_to_peer here, we rely on
		# the caller to check elimination via GameServer.
		# For Phase 1, we return empty and elimination is checked externally.
		pass

	return result

## Determine the winner when time expires.
## Default: the team with alive players wins; if both alive, CT wins.
func _determine_time_expired_winner() -> int:
	if not team_manager or not damage_processor:
		return _td_rm.Team.COUNTER_TERRORIST

	var ct_alive: int = 0
	var t_alive: int = 0

	# Count alive players per team
	for team in [_td_rm.Team.COUNTER_TERRORIST, _td_rm.Team.TERRORIST]:
		var player_ids: Array[int] = team_manager.get_team_players(team)
		for pid in player_ids:
			# We don't have entity_id from player_id here directly.
			# Fallback: just check if team has any players alive
			# This will be refined in future phases with proper entity mapping.
			pass

	# Simple default: CT wins on time expiry
	# (Conceptually, CT defends the site and time runs out = attackers failed)
	return _td_rm.Team.COUNTER_TERRORIST

## Resolve a player_id or entity_id to a team.
## For now, returns -1 if resolution fails.
func _get_entity_team(_entity_or_player_id: int) -> int:
	# In Phase 1, this requires access to entity→peer→player→team mapping.
	# GameServer.entity_to_peer maps entity_id → peer_id.
	# We need peer_id → team from TeamManager.
	# For now, return -1 and rely on GameServer-level elimination checks.
	return -1

## Helper to format winner score description.
func winner_threshold_scores(winning_team: int) -> String:
	if not team_manager:
		return str(win_threshold)
	var ct_score: int = team_manager.get_team_score(_td_rm.Team.COUNTER_TERRORIST)
	var t_score: int = team_manager.get_team_score(_td_rm.Team.TERRORIST)
	return "CT %d — %d T" % [ct_score, t_score]
