extends Node

signal connection_succeeded
signal connection_failed
signal peer_joined(peer_id: int)
signal peer_left(peer_id: int)

const DEFAULT_PORT := 7777
const MAX_PLAYERS := 50  # covers 25v25 casual

# Lightweight ping/population query, answered on a dedicated UDP port
# (game port + a large offset, same "game port vs. query port" split classic
# server browsers use) rather than the ENet game port itself, since ENet's
# own protocol won't respond to arbitrary raw datagrams. Lets
# menu/server_select.gd measure real RTT and live player count for a server
# *before* actually joining it, without touching PlayerRegistry or ENet peer
# state at all. The offset must be big enough that it can't land on another
# server's game port — local dev runs several servers on sequential ports
# (7777, 7778, 7779, ...), so a +1 offset would alias server N's query port
# onto server N+1's actual game port.
const QUERY_PORT_OFFSET := 10000
const PING_REQUEST := "SPACEWAR_PING"

# How often a hosted server re-registers with the matchmaking API as a
# heartbeat. app/config.py's server_stale_seconds (20s) needs enough slack
# over this for one missed beat without flapping a live server to "offline".
const HEARTBEAT_INTERVAL := 8.0

var is_server: bool = false
var target_ip: String = "127.0.0.1"

# IP advertised to the matchmaking API for other players to connect to —
# not necessarily the same as the interface ENet binds to. Defaults to
# loopback for local dev; a real deployment sets --server-ip= to whatever
# address is actually reachable from a player's machine (matches the
# DEV_SEED_SERVER_IP note in matchmaking-api/README.md about this same
# reachability requirement).
var _register_ip: String = "127.0.0.1"
var _hosted_port: int = DEFAULT_PORT
var _heartbeat_timer: Timer

var _query_udp: UDPServer


func _ready() -> void:
	multiplayer.peer_connected.connect(_on_peer_connected)
	multiplayer.peer_disconnected.connect(_on_peer_disconnected)
	multiplayer.connected_to_server.connect(_on_connected_to_server)
	multiplayer.connection_failed.connect(_on_connection_failed)
	multiplayer.server_disconnected.connect(_on_server_disconnected)

	_apply_cmdline_args()


# Dev-only bootstrap: `-- --server` runs this instance as a dedicated headless
# server; `-- --connect=<ip>` overrides the default join target for clients;
# `-- --server-ip=<ip>` overrides the IP a hosted server advertises to the
# matchmaking API (see _register_ip).
func _apply_cmdline_args() -> void:
	var args := OS.get_cmdline_user_args()
	for arg in args:
		if arg.begins_with("--connect="):
			target_ip = arg.substr("--connect=".length())
		elif arg.begins_with("--server-ip="):
			_register_ip = arg.substr("--server-ip=".length())
	if args.has("--server"):
		host_server(DEFAULT_PORT)
		await get_tree().process_frame
		get_tree().change_scene_to_file("res://world/world.tscn")


func host_server(port: int = DEFAULT_PORT) -> void:
	var peer := ENetMultiplayerPeer.new()
	var err := peer.create_server(port, MAX_PLAYERS)
	if err != OK:
		push_error("[NetworkManager] Failed to host on port %d: %s" % [port, err])
		return
	multiplayer.multiplayer_peer = peer
	is_server = true
	_hosted_port = port
	_start_query_responder(port + QUERY_PORT_OFFSET)
	_start_heartbeat()
	print("[NetworkManager] Hosting on port %d" % port)


func disconnect_from_game() -> void:
	if multiplayer.multiplayer_peer != null:
		multiplayer.multiplayer_peer.close()
		multiplayer.multiplayer_peer = null
	is_server = false
	_stop_query_responder()
	_stop_heartbeat()


func _start_heartbeat() -> void:
	_register_with_matchmaking_api()
	_heartbeat_timer = Timer.new()
	_heartbeat_timer.wait_time = HEARTBEAT_INTERVAL
	_heartbeat_timer.one_shot = false
	_heartbeat_timer.timeout.connect(_register_with_matchmaking_api)
	add_child(_heartbeat_timer)
	_heartbeat_timer.start()


func _stop_heartbeat() -> void:
	if _heartbeat_timer != null:
		_heartbeat_timer.queue_free()
		_heartbeat_timer = null


# Fire-and-forget: a missed heartbeat just leaves the API's last_heartbeat
# stale, which its own sweep turns into "offline" after server_stale_seconds
# (see matchmaking-api/app/routers/servers.py) — no retry logic needed here.
func _register_with_matchmaking_api() -> void:
	await MatchmakingClient.register_server(_register_ip, _hosted_port, "casual", PlayerRegistry.players.size(), MAX_PLAYERS)


func _start_query_responder(query_port: int) -> void:
	_query_udp = UDPServer.new()
	var err := _query_udp.listen(query_port)
	if err != OK:
		push_error("[NetworkManager] Failed to start ping responder on port %d: %s" % [query_port, err])
		_query_udp = null
		return
	print("[NetworkManager] Ping responder listening on port %d" % query_port)


func _stop_query_responder() -> void:
	if _query_udp != null:
		_query_udp.stop()
		_query_udp = null


func _process(_delta: float) -> void:
	if _query_udp == null:
		return
	_query_udp.poll()
	while _query_udp.is_connection_available():
		var udp_peer := _query_udp.take_connection()
		var packet := udp_peer.get_packet()
		if packet.get_string_from_utf8() == PING_REQUEST:
			var player_count: int = PlayerRegistry.players.size()
			var reply := "SPACEWAR_PONG:%d:%d" % [player_count, MAX_PLAYERS]
			udp_peer.put_packet(reply.to_utf8_buffer())


func join_server(ip: String, port: int = DEFAULT_PORT) -> void:
	var peer := ENetMultiplayerPeer.new()
	var err := peer.create_client(ip, port)
	if err != OK:
		push_error("[NetworkManager] Failed to start client toward %s:%d: %s" % [ip, port, err])
		connection_failed.emit()
		return
	multiplayer.multiplayer_peer = peer
	is_server = false
	print("[NetworkManager] Connecting to %s:%d..." % [ip, port])


func _on_peer_connected(peer_id: int) -> void:
	print("[NetworkManager] Peer connected: %d" % peer_id)
	peer_joined.emit(peer_id)


func _on_peer_disconnected(peer_id: int) -> void:
	print("[NetworkManager] Peer disconnected: %d" % peer_id)
	peer_left.emit(peer_id)


func _on_connected_to_server() -> void:
	print("[NetworkManager] Connected, local peer id: %d" % multiplayer.get_unique_id())
	connection_succeeded.emit()


func _on_connection_failed() -> void:
	print("[NetworkManager] Connection failed")
	multiplayer.multiplayer_peer = null
	connection_failed.emit()


func _on_server_disconnected() -> void:
	print("[NetworkManager] Server disconnected")
	multiplayer.multiplayer_peer = null
	connection_failed.emit()
