extends Node

# RCON Admin Console — TCP Listener & Auth Layer
# ============================================================================
# TCP-based remote administration for the dedicated game server.
# Source-RCON-inspired protocol, lightweight custom implementation.
#
# Protocol:
#   1. Client connects, first message is the password
#   2. Server responds with "auth_ok\r\n" or "auth_fail\r\n"
#   3. After auth, client sends single-line commands, newline-terminated
#   4. Server responds with multi-line text, terminated by "END\r\n"
#   5. 3 failed auth attempts → disconnect with 5s reconnect penalty
#
# Integration:
#   This is the network layer only. Auth and raw I/O live here.
#   Incoming commands are forwarded to RconCommandHandler for processing.
#   Connect to child node's `rcon_command` signal from game logic
#   to handle game-affecting commands (changelevel, kick, ban, etc.).
#
# Usage (standalone):
#   var rcon = RconServer.new()
#   rcon.enabled = true
#   rcon.port = 28960
#   rcon.password = "mysecret"
#   add_child(rcon)
#
# Usage (with cvar registry — t_p4_config):
#   # Config system reads server.cfg and sets RCON cvars via CvarRegistry
#   # singleton. RconServer auto-detects and reads rcon_enabled, rcon_port,
#   # and rcon_password from it on _ready().
#
# Lifecycle (property-driven):
#   rcon.enabled = true   → calls _start_listening() via setter
#   rcon.enabled = false  → calls _stop_listening() via setter
#   rcon.port = 28961     → re-binds on new port if currently listening

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
var enabled: bool = false:
	set = set_enabled
var port: int = 28960:
	set = set_port
var password: String = "changeme":
	set = set_password

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
const AUTH_PENDING: int = 0
const AUTHENTICATED: int = 1
const MAX_AUTH_FAILS: int = 3
const AUTH_PENALTY_SECONDS: float = 5.0
const RESP_END: String = "END\r\n"
const MAX_BUFFER_SIZE: int = 4096
const HEARTBEAT_INTERVAL: float = 30.0

# ---------------------------------------------------------------------------
# Internal state
# ---------------------------------------------------------------------------
var _tcp_server: TCPServer = TCPServer.new()
var _connections: Dictionary = {}  # StreamPeerTCP -> ConnectionState
var _banned_ips: Dictionary = {}   # "ip" -> float (unban timestamp)
var _command_handler: Node = null
var _heartbeat_timer: float = 0.0

# ---------------------------------------------------------------------------
# Initialization
# ---------------------------------------------------------------------------
func _ready() -> void:
	# Try to load password from file
	_load_password_from_file()
	
	# Children added in scene file take priority
	for child in get_children():
		if child is Node and child.has_method("handle_command"):
			_command_handler = child
			break
	
	# Auto-load command handler if not already added as child
	if not _command_handler:
		var handler_path = "res://server/scripts/rcon_command_handler.gd"
		if ResourceLoader.exists(handler_path):
			var handler = load(handler_path).new()
			add_child(handler)
			_command_handler = handler
	
	# Apply overrides from CvarRegistry singleton (t_p4_config integration)
	_apply_cvar_overrides()
	
	if enabled:
		_start_listening()

func _exit_tree() -> void:
	_stop_listening()

# ---------------------------------------------------------------------------
# Process loop — poll network, accept connections, handle I/O
# ---------------------------------------------------------------------------
func _process(delta: float) -> void:
	if not enabled or not _tcp_server or not _tcp_server.is_listening():
		return
	
	_heartbeat_timer += delta
	
	# --- Accept new connections ---
	while _tcp_server.is_connection_available():
		var stream: StreamPeerTCP = _tcp_server.take_connection()
		if not stream:
			continue
		
		var ip: String = stream.get_connected_address()
		
		# Check if IP is under auth penalty
		if _is_ip_banned(ip):
			_send_raw(stream, "Server busy — try again later.\r\n")
			_send_end(stream)
			stream.disconnect_from_host()
			continue
		
		_connections[stream] = {
			"state": AUTH_PENDING,
			"buffer": "",
			"fail_count": 0,
			"ip": ip
		}
	
	# --- Poll existing connections ---
	var to_remove: Array = []
	for stream in _connections:
		var conn = _connections[stream]
		var status: int = stream.get_status()
		
		if status == StreamPeerTCP.STATUS_NONE or status == StreamPeerTCP.STATUS_ERROR:
			to_remove.append(stream)
			continue
		
		if status != StreamPeerTCP.STATUS_CONNECTED:
			continue
		
		# Read available data
		var avail: int = stream.get_available_bytes()
		if avail <= 0:
			continue
		
		var result: Array = stream.get_data(avail)
		if result[0] != OK:
			to_remove.append(stream)
			continue
		
		conn["buffer"] += result[1].get_string_from_utf8()
		
		# Buffer overflow protection
		if conn["buffer"].length() > MAX_BUFFER_SIZE:
			_send_raw(stream, "Buffer overflow — disconnected.\r\n")
			_send_end(stream)
			to_remove.append(stream)
			continue
		
		# Process complete lines
		while "\n" in conn["buffer"]:
			var idx: int = conn["buffer"].find("\n")
			var line: String = conn["buffer"].substr(0, idx).strip_edges()
			conn["buffer"] = conn["buffer"].substr(idx + 1)
			
			if line.is_empty():
				continue
			
			if conn["state"] == AUTH_PENDING:
				_handle_auth(stream, conn, line)
			else:
				_handle_command(stream, line)
	
	# Cleanup disconnected clients
	for stream in to_remove:
		_cleanup_connection(stream)

# ---------------------------------------------------------------------------
# Auth handling
# ---------------------------------------------------------------------------
func _handle_auth(stream: StreamPeerTCP, conn: Dictionary, line: String) -> void:
	if line == password:
		conn["state"] = AUTHENTICATED
		_send_raw(stream, "auth_ok\r\n")
		_send_end(stream)
	else:
		conn["fail_count"] += 1
		var remaining: int = MAX_AUTH_FAILS - conn["fail_count"]
		_send_raw(stream, "auth_fail\r\n")
		if remaining > 0:
			_send_raw(stream, str(remaining) + " attempt(s) remaining.\r\n")
		_send_end(stream)
		
		if conn["fail_count"] >= MAX_AUTH_FAILS:
			# Ban IP temporarily
			var ip: String = conn["ip"]
			_banned_ips[ip] = Time.get_unix_time_from_system() + AUTH_PENALTY_SECONDS
			stream.disconnect_from_host()
			print("[RCON] Auth failed 3 times from " + ip + " — penalty for " + str(AUTH_PENALTY_SECONDS) + "s")

# ---------------------------------------------------------------------------
# Command dispatch
# ---------------------------------------------------------------------------
func _handle_command(stream: StreamPeerTCP, line: String) -> void:
	var parts: PackedStringArray = line.split(" ", false)
	if parts.is_empty():
		return
	
	var cmd: String = parts[0].to_lower()
	var args: PackedStringArray = parts.slice(1)
	
	# Route to command handler
	if _command_handler and _command_handler.has_method("handle_command"):
		var response: String = _command_handler.handle_command(cmd, args)
		_send_raw(stream, response)
		_send_end(stream)
	else:
		_send_raw(stream, "Command handler not available.\r\n")
		_send_end(stream)

# ---------------------------------------------------------------------------
# Response helpers
# ---------------------------------------------------------------------------
func _send_raw(stream: StreamPeerTCP, text: String) -> void:
	var data: PackedByteArray = text.to_utf8_buffer()
	stream.put_data(data)

func _send_end(stream: StreamPeerTCP) -> void:
	_send_raw(stream, RESP_END)

# ---------------------------------------------------------------------------
# IP ban / penalty
# ---------------------------------------------------------------------------
func _is_ip_banned(ip: String) -> bool:
	if not _banned_ips.has(ip):
		return false
	if Time.get_unix_time_from_system() >= _banned_ips[ip]:
		_banned_ips.erase(ip)
		return false
	return true

func _cleanup_connection(stream: StreamPeerTCP) -> void:
	if _connections.has(stream):
		_connections.erase(stream)
	if stream.get_status() == StreamPeerTCP.STATUS_CONNECTED:
		stream.disconnect_from_host()

# ---------------------------------------------------------------------------
# Server lifecycle — called from setters, _ready(), and external code
# ---------------------------------------------------------------------------
func _start_listening() -> void:
	if _tcp_server and _tcp_server.is_listening():
		return
	
	if not _tcp_server:
		_tcp_server = TCPServer.new()
	
	var err: int = _tcp_server.listen(port)
	if err != OK:
		push_error("[RCON] Failed to listen on port " + str(port) + " (error " + str(err) + ")")
		enabled = false
		return
	
	print("[RCON] Listening on port " + str(port))

func _stop_listening() -> void:
	if _tcp_server and _tcp_server.is_listening():
		_tcp_server.stop()
	
	# Disconnect all clients gracefully
	for stream in _connections:
		if stream.get_status() == StreamPeerTCP.STATUS_CONNECTED:
			_send_raw(stream, "Server shutting down.\r\n")
			_send_end(stream)
			stream.disconnect_from_host()
	_connections.clear()
	
	print("[RCON] Stopped")

# ---------------------------------------------------------------------------
# Config file loading
# ---------------------------------------------------------------------------
func _load_password_from_file() -> void:
	var pw_path: String = "res://server/data/rcon_password.cfg"
	if not ResourceLoader.exists(pw_path):
		return
	
	var file: FileAccess = FileAccess.open(pw_path, FileAccess.READ)
	if not file:
		return
	
	var line: String = file.get_line().strip_edges()
	if not line.is_empty() and not line.begins_with("#"):
		password = line
		print("[RCON] Password loaded from " + pw_path)
	file.close()

# ---------------------------------------------------------------------------
# Config overrides from cvar registry (if available)
# ---------------------------------------------------------------------------
func _apply_cvar_overrides() -> void:
	if not has_node("/root/CvarRegistry"):
		return
	
	var registry = get_node("/root/CvarRegistry")
	if registry and registry.has_method("get"):
		var v = registry.get("rcon_enabled")
		if v != null:
			enabled = bool(v)
		v = registry.get("rcon_port")
		if v != null:
			port = int(v)
		v = registry.get("rcon_password")
		if v != null:
			password = str(v)

# ---------------------------------------------------------------------------
# Setters — react to property changes
# ---------------------------------------------------------------------------
func set_enabled(val: bool) -> void:
	enabled = val
	if Engine.is_editor_hint():
		return
	if enabled:
		_start_listening()
	else:
		_stop_listening()

func set_port(val: int) -> void:
	port = clampi(val, 1024, 65535)
	if enabled and _tcp_server and _tcp_server.is_listening():
		_stop_listening()
		_start_listening()

func set_password(val: String) -> void:
	password = val
