## MapVote — Server-Side Map Change Voting System
##
## Handles the voting lifecycle when a player requests a map change.
## Follows simple majority rules: >50% of connected players must accept
## within 30 seconds for the vote to pass.
##
## ## Vote Lifecycle
##
##   Idle ──vote_start()──► Voting (30s timer)
##                             ├── vote(accept=true)  → tally yes
##                             ├── vote(accept=false) → tally no
##                             ├── time expires       → check majority
##                             │   ├── >50% yes       → vote_passed
##                             │   └── <=50% yes      → vote_failed
##                             └── vote_cancel()      → vote_cancelled
##
## ## Threshold
##
##   Vote passes when:  yes_count > (total_connected / 2)
##   Tie or less → fails.
##   Abstentions (no vote) count as "no" but only yes/no tallies are tracked.
##
## ## Integration
##
##   Instantiate as a child of ServerMain or the map browser system.
##   Connect to MapBrowserRPC.map_change_requested → MapVote.vote_start()
##
## ## Signals
##
##   vote_started(map_id, initiator_id) — emitted when a vote begins
##   vote_passed(map_id)                — emitted when vote passes
##   vote_failed(map_id)                — emitted when vote fails
##   vote_cancelled(map_id)             — emitted when vote is cancelled
##   vote_tally_updated(yes, no, total) — emitted each time a vote is cast
##
## =============================================================================

extends Node

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

## Emitted when a vote begins.
signal vote_started(map_id: String, initiator_id: int)

## Emitted when the vote passes (>50% yes).
signal vote_passed(map_id: String)

## Emitted when the vote fails (≤50% yes or timeout).
signal vote_failed(map_id: String)

## Emitted when the vote is cancelled externally.
signal vote_cancelled(map_id: String)

## Emitted each time a player casts a vote, for UI updates.
signal vote_tally_updated(yes_count: int, no_count: int, total_players: int)

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

## Vote duration in seconds.
const VOTE_DURATION: float = 30.0

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

## The map ID currently being voted on (empty string if no active vote).
var active_map_id: String = ""

## The peer ID of the player who initiated the vote.
var initiator_peer_id: int = -1

## Whether a vote is currently in progress.
var is_voting: bool = false : get = _is_voting

## Timer node for vote countdown.
var _vote_timer: SceneTreeTimer = null

## Yes votes: peer_id → true
var _yes_votes: Dictionary = {}

## No votes: peer_id → true
var _no_votes: Dictionary = {}

# ---------------------------------------------------------------------------
# Getters
# ---------------------------------------------------------------------------

func _is_voting() -> bool:
	return is_voting

## Get the current yes count.
func get_yes_count() -> int:
	return _yes_votes.size()

## Get the current no count.
func get_no_count() -> int:
	return _no_votes.size()

## Get total connected players (from NetworkManager's peer list).
func _get_total_players() -> int:
	if not multiplayer.has_multiplayer_peer():
		return 0
	return multiplayer.get_peers().size() + 1  # +1 for the server itself

# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

## Start a vote for the given map_id, initiated by initiator_id.
## If a vote is already in progress, the request is ignored (prints a warning).
func vote_start(map_id: String, initiator_id: int) -> void:
	if is_voting:
		print("[MapVote] Vote already in progress for '%s' — ignoring request for '%s'" % [active_map_id, map_id])
		return

	# Validate map exists via WorkshopBrowser if available
	if WorkshopBrowser and WorkshopBrowser.has_method(&"has_map"):
		if not WorkshopBrowser.has_map(map_id):
			print("[MapVote] Cannot start vote: unknown map '%s'" % map_id)
			return

	active_map_id = map_id
	initiator_peer_id = initiator_id
	is_voting = true
	_yes_votes.clear()
	_no_votes.clear()

	# Auto-register the initiator's yes vote
	_yes_votes[initiator_id] = true

	print("[MapVote] Vote started: map='%s' initiated by peer %d" % [map_id, initiator_id])
	vote_started.emit(map_id, initiator_id)
	vote_tally_updated.emit(_yes_votes.size(), _no_votes.size(), _get_total_players())

	# Start the 30-second countdown timer
	_vote_timer = get_tree().create_timer(VOTE_DURATION)
	_vote_timer.timeout.connect(_on_vote_timeout)

## Cast a vote. Each player may vote once: accept=true for yes, false for no.
## If the player has already voted, their vote is updated (last vote wins).
## Returns true if the vote was accepted, false if no active vote.
func vote(player_id: int, accept: bool) -> bool:
	if not is_voting:
		return false

	# Remove any prior vote from this player
	_yes_votes.erase(player_id)
	_no_votes.erase(player_id)

	if accept:
		_yes_votes[player_id] = true
	else:
		_no_votes[player_id] = true

	print("[MapVote] Peer %d voted %s for map '%s'" % [player_id, "yes" if accept else "no", active_map_id])
	vote_tally_updated.emit(_yes_votes.size(), _no_votes.size(), _get_total_players())
	return true

## Cancel the current vote (e.g., admin override via RCON).
## The vote is cancelled regardless of time remaining.
func vote_cancel() -> void:
	if not is_voting:
		return

	var cancelled_map: String = active_map_id
	_cleanup()
	print("[MapVote] Vote cancelled for map '%s'" % cancelled_map)
	vote_cancelled.emit(cancelled_map)

## Check if a specific player has voted.
func has_player_voted(player_id: int) -> bool:
	return _yes_votes.has(player_id) or _no_votes.has(player_id)

## Get the vote direction for a player ("yes", "no", or "" if not voted).
func get_player_vote(player_id: int) -> String:
	if _yes_votes.has(player_id):
		return "yes"
	if _no_votes.has(player_id):
		return "no"
	return ""

# ---------------------------------------------------------------------------
# Internal
# ---------------------------------------------------------------------------

## Called when the 30-second vote timer expires.
func _on_vote_timeout() -> void:
	if not is_voting:
		return

	var yes: int = _yes_votes.size()
	var no: int = _no_votes.size()
	var total: int = _get_total_players()
	var threshold: float = total * 0.5

	print("[MapVote] Vote ended: map='%s' yes=%d no=%d total=%d threshold=%.1f" % [active_map_id, yes, no, total, threshold])

	if yes > threshold:
		var passed_map: String = active_map_id
		_cleanup()
		print("[MapVote] Vote PASSED for map '%s' (%d yes > %d total / 2)" % [passed_map, yes, total])
		vote_passed.emit(passed_map)

		# Broadcast result to all clients
		if MapBrowserRPC and MapBrowserRPC.has_method(&"broadcast_vote_result"):
			MapBrowserRPC.broadcast_vote_result.rpc(passed_map, true, yes, no)
	else:
		var failed_map: String = active_map_id
		_cleanup()
		print("[MapVote] Vote FAILED for map '%s' (%d yes ≤ %d total / 2)" % [failed_map, yes, total])
		vote_failed.emit(failed_map)

		# Broadcast result to all clients
		if MapBrowserRPC and MapBrowserRPC.has_method(&"broadcast_vote_result"):
			MapBrowserRPC.broadcast_vote_result.rpc(failed_map, false, yes, no)

## Clean up vote state.
func _cleanup() -> void:
	active_map_id = ""
	initiator_peer_id = -1
	is_voting = false
	_yes_votes.clear()
	_no_votes.clear()
	_vote_timer = null

## Clean up on exit, cancelling any active vote.
func _exit_tree() -> void:
	if is_voting:
		vote_cancel()
