extends Node

# RCON Admin Console — Command Handler
# ============================================================================
# Processes RCON commands, returns formatted response strings.
# Game-affecting commands (changelevel, kick, ban, say, players, etc.)
# are dispatched via the `rcon_command` signal so game logic can handle them
# without coupling RCON to game-specific systems.
#
# Commands handled inline (no game dependency):
#   help, echo/ping, status (engine-level stats), quit
#   cvarlist, cvar_get, cvar_set (via CvarRegistry singleton if available)
#
# Plugin commands are delegated to PluginManager singleton:
#   plugin list, plugin load, plugin unload, plugin reload,
#   plugin info, plugin rescan
#
# Commands dispatched via signal:
#   changelevel, kick, ban, unban, say, msg, players, exec
#
# Connection pattern for game logic:
#   func _ready():
#       var rcon = get_node("/root/RconServer/RconCommandHandler")
#       if rcon:
#           rcon.rcon_command.connect(_on_rcon_command)
#
#   func _on_rcon_command(command: String, args: PackedStringArray):
#       match command:
#           "changelevel":
#               $ServerManager.change_level(args[0])
#           "kick":
#               $PlayerManager.kick_player(args[0])

# ---------------------------------------------------------------------------
# Signals
# ---------------------------------------------------------------------------
signal rcon_command(command: String, args: PackedStringArray)

# ---------------------------------------------------------------------------
# Public API — called by rcon_server.gd
# ---------------------------------------------------------------------------
func handle_command(cmd: String, args: PackedStringArray) -> String:
	match cmd:
		"help":
			return _cmd_help(args)
		"echo", "ping":
			return _cmd_echo(args)
		"status":
			return _cmd_status(args)
		"quit":
			return _cmd_quit(args)
		"cvarlist":
			return _cmd_cvarlist(args)
		"cvar_get":
			return _cmd_cvar_get(args)
		"cvar_set":
			return _cmd_cvar_set(args)
		"plugin":
			return _cmd_plugin(args)
		"say", "msg", "players", "changelevel", "kick", "ban", "unban", "exec", "start_match", "end_round":
			return _cmd_dispatch(cmd, args)
		_:
			return "Unknown command: '" + cmd + "'. Type 'help' for available commands.\\r\\n"

# ---------------------------------------------------------------------------
# Built-in commands
# ---------------------------------------------------------------------------
func _cmd_help(_args: PackedStringArray) -> String:
	var lines: PackedStringArray = [
		"=== RCON Commands ===",
		"  help                        Show this help",
		"  echo [text]                 Echo text back (ping test)",
		"  status                      Show server engine status",
		"  players                     List connected players",
		"  say <message>               Broadcast chat to all players",
		"  msg <player> <message>      Send private message",
		"  changelevel <map_name>      Load a new map",
		"  start_match                 Start the match (exit warmup)",
		"  end_round <team> [reason]   Force-end the current round",
		"  kick <player> [reason]      Remove a player",
		"  ban <player> [duration]     Ban a player from the server",
		"  unban <player_or_steamid>   Remove a ban",
		"  exec <config_file>          Execute a server config file",
		"  cvarlist [pattern]          List cvars (optional filter)",
		"  cvar_get <name>             Get a cvar's value",
		"  cvar_set <name> <value>     Set a writable cvar",
		"  plugin <subcommand> [args]  Plugin management (list/load/unload/reload/info/rescan)",
		"  quit                        Shut down the server gracefully",
	]
	return "\r\n".join(lines) + "\r\n"

func _cmd_echo(args: PackedStringArray) -> String:
	if args.is_empty():
		return "Pong\r\n"
	return " ".join(args) + "\\r\\n"

func _cmd_status(_args: PackedStringArray) -> String:
	var lines: PackedStringArray = []
	lines.append("=== Server Status ===")
	
	# Engine version
	var vi: Dictionary = Engine.get_version_info()
	lines.append("  Engine:    Godot " + str(vi.get("major", "?")) + "." + str(vi.get("minor", "?")) + "." + str(vi.get("patch", "?")))
	
	# FPS
	lines.append("  FPS:       " + str(Engine.get_frames_per_second()))
	
	# Physics tick rate
	lines.append("  Tickrate:  " + str(Engine.physics_ticks_per_second) + " Hz")
	
	# Uptime
	var uptime_ms: int = Time.get_ticks_msec()
	var total_sec: int = uptime_ms / 1000
	var h: int = total_sec / 3600
	var m: int = (total_sec % 3600) / 60
	var s: int = total_sec % 60
	lines.append("  Uptime:    " + str(h) + "h " + str(m) + "m " + str(s) + "s")
	
	# Server time
	var dt: Dictionary = Time.get_datetime_dict_from_system()
	lines.append("  Time:      " + \
		str(dt.get("year", 0)) + "-" + str(dt.get("month", 1)).pad_zeros(2) + "-" + str(dt.get("day", 1)).pad_zeros(2) + " " + \
		str(dt.get("hour", 0)).pad_zeros(2) + ":" + str(dt.get("minute", 0)).pad_zeros(2) + ":" + str(dt.get("second", 0)).pad_zeros(2))
	
	# Performance metrics
	lines.append("  Objects:   " + str(Performance.get_monitor(Performance.OBJECT_COUNT)))
	var mem_kb: float = Performance.get_monitor(Performance.MEMORY_STATIC)
	lines.append("  Memory:    " + str(snapped(mem_kb / 1024.0, 0.1)) + " MB")
	
	# Node count (via SceneTree)
	lines.append("  Nodes:     " + str(get_tree().node_count))
	
	# RCON info
	lines.append("  RCON:      enabled on port ? (see rcon_server)")
	
	# Try to get server name from engine
	var server_name: String = ProjectSettings.get_setting("application/config/name", "Tactical Shooter")
	lines.append("  Server:    " + server_name)
	
	return "\r\n".join(lines) + "\r\n"

func _cmd_quit(_args: PackedStringArray) -> String:
	# Defer shutdown to next frame so the response can be sent
	get_tree().call_deferred("quit")
	return "Server shutting down...\r\n"

func _cmd_cvarlist(args: PackedStringArray) -> String:
	var pattern: String = args[0] if args.size() > 0 else ""
	
	if not has_node("/root/CvarRegistry"):
		if pattern.is_empty() or "rcon" in pattern:
			return "rcon_enabled: bool\r\nrcon_port: int\r\nrcon_password: string [PW]\r\n(Cvar registry not available — RCON cvars shown)\r\n"
		return "Cvar registry not available. No cvars match '" + pattern + "'.\r\n"
	
	var registry = get_node("/root/CvarRegistry")
	if not registry or not registry.has_method("list_cvars"):
		return "Cvar registry available but incompatible.\r\n"
	
	var cvar_names: Array = registry.list_cvars()
	var lines: PackedStringArray = []
	lines.append("=== Cvar List" + (" (filter: '" + pattern + "')" if pattern else "") + " ===")
	
	var found: int = 0
	for cvar_name in cvar_names:
		if not pattern.is_empty() and pattern not in str(cvar_name):
			continue
		
		var val = registry.get(cvar_name)
		var flags: int = 0
		var cvar_type: String = "?"
		
		if registry.has_method("get_flags"):
			flags = registry.get_flags(cvar_name)
		if registry.has_method("get_type"):
			cvar_type = str(registry.get_type(cvar_name))
		
		var readonly: String = " [RO]" if (flags & 1) else ""
		var passwd: String = " [PW]" if str(cvar_name).find("password") >= 0 else ""
		lines.append("  " + str(cvar_name) + " = " + str(val) + " (" + cvar_type + ")" + readonly + passwd)
		found += 1
	
	if found == 0:
		return "No cvars match '" + pattern + "'.\r\n"
	
	lines.append(str(found) + " cvars listed.")
	return "\r\n".join(lines) + "\r\n"

func _cmd_cvar_get(args: PackedStringArray) -> String:
	if args.is_empty():
		return "Usage: cvar_get <name>\r\n"
	
	var name: String = args[0]
	
	if has_node("/root/CvarRegistry"):
		var registry = get_node("/root/CvarRegistry")
		if registry and registry.has_method("get"):
			var val = registry.get(name)
			if val != null:
				return name + " = " + str(val) + "\r\n"
			return "Cvar not found: " + name + "\r\n"
	
	return "Cvar registry not available.\r\n"

func _cmd_cvar_set(args: PackedStringArray) -> String:
	if args.size() < 2:
		return "Usage: cvar_set <name> <value>\r\n"
	
	var name: String = args[0]
	var value: String = args[1]
	
	if has_node("/root/CvarRegistry"):
		var registry = get_node("/root/CvarRegistry")
		if registry and registry.has_method("set"):
			# Use Callable to avoid compile-time void return detection on dynamic object
			var fn = Callable(registry, "set")
			var err = fn.call(name, value)
			if err == OK:
				return name + " = " + value + "\r\n"
			match err:
				ERR_INVALID_PARAMETER:
					return "Cvar not found or read-only: " + name + "\r\n"
				ERR_INVALID_DATA:
					return "Invalid value for " + name + ": " + value + "\r\n"
				_:
					return "Failed to set " + name + " (error " + str(err) + ")\r\n"
	
	return "Cvar registry not available.\r\n"

# ---------------------------------------------------------------------------
# Plugin management — delegates to PluginManager singleton
# ---------------------------------------------------------------------------
func _cmd_plugin(args: PackedStringArray) -> String:
	# Try to find PluginManager singleton (autoload)
	var pm = get_node("/root/PluginManager")
	if not pm:
		return "PluginManager not available. Ensure PluginManager is registered as an autoload in project.godot.\r\n"

	if not pm.has_method("rcon_command"):
		return "PluginManager does not support RCON commands.\r\n"

	return pm.rcon_command(args)

# ---------------------------------------------------------------------------
# Delegated commands — emit signal for game logic to handle
# ---------------------------------------------------------------------------
func _cmd_dispatch(cmd: String, args: PackedStringArray) -> String:
	rcon_command.emit(cmd, args)
	return "Command '" + cmd + "' dispatched to server.\r\n"


# ---------------------------------------------------------------------------
# Plugin management commands — interfaces with PluginManager singleton
# ---------------------------------------------------------------------------
func _cmd_plugins(args: PackedStringArray) -> String:
	"""Handle 'plugins' command with subcommands."""
	if args.is_empty():
		return _cmd_plugins_help()
	
	var subcmd: String = args[0].to_lower()
	
	match subcmd:
		"list":
			return _cmd_plugins_list()
		"load":
			if args.size() < 2:
				return "Usage: plugins load <name>\r\n"
			return _cmd_plugins_load(args[1])
		"unload":
			if args.size() < 2:
				return "Usage: plugins unload <name>\r\n"
			return _cmd_plugins_unload(args[1])
		"reload":
			if args.size() < 2:
				return "Usage: plugins reload <name>\r\n"
			return _cmd_plugins_reload(args[1])
		"info":
			if args.size() < 2:
				return "Usage: plugins info <name>\r\n"
			return _cmd_plugins_info(args[1])
		"scan", "rescan":
			return _cmd_plugins_rescan()
		_:
			return "Unknown plugins subcommand: '" + subcmd + "'\r\n" + _cmd_plugins_help()


func _cmd_plugins_help() -> String:
	return "\r\n".join([
		"=== Plugin Management ===",
		"  plugins list            List all loaded plugins",
		"  plugins load <name>     Load a plugin by name from disk",
		"  plugins unload <name>   Unload a running plugin",
		"  plugins reload <name>   Reload a plugin (hotswap)",
		"  plugins info <name>     Show metadata for a plugin",
		"  plugins rescan          Rescan plugin directory for new manifests",
	]) + "\r\n"


func _cmd_plugins_list() -> String:
	var mgr = _get_plugin_manager()
	if not mgr:
		return "Plugin system not available (no PluginManager singleton).\r\n"
	
	var plugins = mgr.list_plugins()
	if plugins.is_empty():
		return "No plugins loaded.\r\n"
	
	var lines: PackedStringArray = []
	lines.append("=== Loaded Plugins (" + str(plugins.size()) + ") ===")
	lines.append("  %-24s %-10s %s" % ["Name", "Version", "Author"])
	lines.append("  " + "-".repeat(50))
	
	for p in plugins:
		lines.append("  %-24s %-10s %s" % [p.name, p.version, p.author])
	
	return "\r\n".join(lines) + "\r\n"


func _cmd_plugins_load(name: String) -> String:
	var mgr = _get_plugin_manager()
	if not mgr:
		return "Plugin system not available.\r\n"
	
	if not mgr.has_method("discover_plugins"):
		return "PluginManager does not support targeted loading yet — use 'plugins rescan' to reload all.\r\n"
	
	if mgr.get_plugin(name):
		return "Plugin '" + name + "' is already loaded.\r\n"
	
	var result = mgr.discover_plugins()
	if mgr.get_plugin(name):
		return "Plugin '" + name + "' loaded successfully.\r\n"
	else:
		return "Failed to load '" + name + "'. Check that plugin.tres exists in user://plugins/" + name.to_lower() + "/\r\n"


func _cmd_plugins_unload(name: String) -> String:
	var mgr = _get_plugin_manager()
	if not mgr:
		return "Plugin system not available.\r\n"
	
	if not mgr.has_method("unload_plugin"):
		return "PluginManager does not support unloading.\r\n"
	
	if mgr.unload_plugin(name):
		return "Plugin '" + name + "' unloaded.\r\n"
	else:
		return "Plugin '" + name + "' not found. Use 'plugins list' to see loaded plugins.\r\n"


func _cmd_plugins_reload(name: String) -> String:
	var mgr = _get_plugin_manager()
	if not mgr:
		return "Plugin system not available.\r\n"
	
	if not mgr.has_method("reload_plugin"):
		return "PluginManager does not support reloading.\r\n"
	
	if mgr.reload_plugin(name):
		return "Plugin '" + name + "' reloaded.\r\n"
	else:
		return "Plugin '" + name + "' not found. Use 'plugins list' to see loaded plugins.\r\n"


func _cmd_plugins_info(name: String) -> String:
	var mgr = _get_plugin_manager()
	if not mgr:
		return "Plugin system not available.\r\n"
	
	var plugin = mgr.get_plugin(name)
	if not plugin:
		return "Plugin '" + name + "' not found. Use 'plugins list' to see loaded plugins.\r\n"
	
	var lines: PackedStringArray = []
	lines.append("=== Plugin: " + plugin.plugin_name + " ===")
	lines.append("  Name:        " + plugin.plugin_name)
	lines.append("  Version:     " + plugin.plugin_version)
	lines.append("  Author:      " + plugin.plugin_author)
	lines.append("  Description: " + plugin.plugin_description)
	lines.append("  Manifest:    " + plugin.plugin_manifest_path)
	return "\r\n".join(lines) + "\r\n"


func _cmd_plugins_rescan() -> String:
	var mgr = _get_plugin_manager()
	if not mgr:
		return "Plugin system not available.\r\n"
	
	var result = mgr.discover_plugins()
	return "Rescan complete — loaded: " + str(result.get("loaded", 0)) + \
		", failed: " + str(result.get("failed", 0)) + \
		", skipped: " + str(result.get("skipped", 0)) + "\r\n"


# ---------------------------------------------------------------------------
# Helper to locate PluginManager singleton
# ---------------------------------------------------------------------------
func _get_plugin_manager():
	"""Get the PluginManager singleton, or null if not available."""
	if Engine.has_singleton("PluginManager"):
		return Engine.get_singleton("PluginManager")
	if has_node("/root/PluginManager"):
		return get_node("/root/PluginManager")
	return null
