## ServerConfig — CFG/JSON Dual-Format Config Manager
##
## Autoload singleton that loads, validates, and exposes server configuration.
##
## ## Architecture
##
##   Load chain:
##     1. SERVER_CFG env var → absolute path to override .cfg
##     2.  user://server_config.cfg  (writable, operator-edited)
##     3.  res://config/default_server_config.cfg  (bundled, read-only)
##
##   Exports:
##     - Typed GDScript properties (this script)
##     - JSON file written alongside the loaded .cfg for automation
##     - ConfigChanged signal for hot-reload (future)
##
## ## Dual Format
##
##   Humans edit `.cfg` (INI-style, Godot ConfigFile parser).
##   Automation reads `.json` (written on every load/save).
##
##   File:   server_config.cfg  ← human editor
##   Mirror: server_config.json ← machine consumer (written atomically)
##
## ## Sections
##
##   [server]   — Network bind, port, name, password, tick_rate
##   [game]     — Round timers, gravity, friendly fire, respawn
##   [movement] — Walk/sprint/crouch speeds, acceleration, friction
##   [match]    — Win limit, time limit, map rotation
##   [rcon]     — Remote console (future)
##   [logging]  — Log level and file path
##   [teams]    — Team count and size
##
## ## Extension Pattern
##
##   ServerMain uses ServerConfig at startup. Each subsystem accesses
##   the singleton directly — no pass-through boilerplate:
##
##       ServerConfig.port             # int
##       ServerConfig.server_name        # String
##       ServerConfig.tick_rate          # int
##       ServerConfig.movement_walk_speed  # float
##       ServerConfig.map_list           # Array[String]
##       ServerConfig.to_json_string()   # String
##
## =============================================================================

extends Node

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

## Emitted when the config is (re)loaded. Subsystems that cache config
## values should reconnect to this signal to pick up live changes.
signal config_loaded()


# ---------------------------------------------------------------------------
# Section: [server]
# ---------------------------------------------------------------------------

var server_name: String = "Tactical Shooter Server"
var description: String = ""
var bind_ip: String = "0.0.0.0"
var port: int = 34197
var max_players: int = 16
var password: String = ""
var tick_rate: int = 128


# ---------------------------------------------------------------------------
# Section: [game]
# ---------------------------------------------------------------------------

var round_time_seconds: int = 600
var warmup_time_seconds: int = 60
var friendly_fire: bool = false
var ff_damage_multiplier: float = 0.5
var gravity: float = -24.0
var respawn_time_seconds: float = 5.0
var spectate_enabled: bool = true


# ---------------------------------------------------------------------------
# Section: [movement]
# ---------------------------------------------------------------------------

var movement_walk_speed: float = 5.0
var movement_sprint_speed: float = 7.0
var movement_crouch_speed: float = 2.0
var movement_jump_velocity: float = 6.0
var movement_acceleration: float = 20.0
var movement_air_acceleration: float = 4.0
var movement_friction: float = 8.0


# ---------------------------------------------------------------------------
# Section: [match]
# ---------------------------------------------------------------------------

var win_limit: int = 3
var time_limit_seconds: int = 0
var map_rotation_mode: String = "sequence"  # "sequence" | "vote" | "random"
var maps: String = "test_range"               # comma-separated

## Parsed map list (populated after load).
var map_list: Array[String] = []


# ---------------------------------------------------------------------------
# Section: [rcon]
# ---------------------------------------------------------------------------

var rcon_enabled: bool = false
var rcon_password: String = ""
var rcon_port: int = 34198


# ---------------------------------------------------------------------------
# Section: [logging]
# ---------------------------------------------------------------------------

var log_level: String = "info"
var log_file: String = ""


# ---------------------------------------------------------------------------
# Section: [teams]
# ---------------------------------------------------------------------------

var team_count: int = 2
var players_per_team: int = 8


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

## Absolute path to the loaded .cfg file (user override or bundled default).
var config_path: String = "" : get = get_config_path
## Absolute path to the mirrored .json file.
var json_path: String = "" : get = get_json_path

var _loaded: bool = false
var _config_path_resolved: String = ""
var _json_path_resolved: String = ""


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

func _ready() -> void:
	call_deferred(&"_load_config")


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

## Reload the config from disk. Emits config_loaded on success.
func reload() -> bool:
	return _load_config()


## Return the entire config as a formatted JSON string.
func to_json_string() -> String:
	var data := _build_json_dict()
	return JSON.stringify(data, "\t", false)


## Return the entire config as a Godot Dictionary (same shape as JSON).
func to_dict() -> Dictionary:
	return _build_json_dict()


## Return the movement config as a Dictionary (for SimulationServer.set_movement_config()).
func make_movement_dict() -> Dictionary:
	return {
		walk_speed = movement_walk_speed,
		sprint_speed = movement_sprint_speed,
		crouch_speed = movement_crouch_speed,
		acceleration = movement_acceleration,
		air_acceleration = movement_air_acceleration,
		friction = movement_friction,
		jump_velocity = movement_jump_velocity,
		gravity = gravity,
	}


## Write the current config state to the JSON mirror file.
## Does NOT touch the .cfg (only humans edit the .cfg).
func save_json() -> Error:
	if _json_path_resolved.is_empty():
		return ERR_FILE_BAD_PATH

	var f := FileAccess.open(_json_path_resolved, FileAccess.WRITE)
	if f == null:
		push_error("[ServerConfig] Failed to write JSON mirror: %s" % _json_path_resolved)
		return FileAccess.get_open_error()

	f.store_string(to_json_string())
	f.close()
	return OK


## Write the current config state to the .cfg file (serialises all sections).
## Returns OK on success, ERR_* on failure.
func save_cfg() -> Error:
	var cfg := ConfigFile.new()
	_populate_cfg(cfg)

	var path := _config_path_resolved if not _config_path_resolved.is_empty() else "user://server_config.cfg"
	var err := cfg.save(path)
	if err != OK:
		push_error("[ServerConfig] Failed to save config: %s" % error_string(err))
		return err

	# Re-init from file to ensure in-memory state matches
	_load_from_path(cfg, _config_path_resolved)
	return OK


# ---------------------------------------------------------------------------
# Internal: Loading
# ---------------------------------------------------------------------------

func _load_config() -> bool:
	# Determine config source path
	var cfg_path := _resolve_source_path()
	if cfg_path.is_empty():
		push_error("[ServerConfig] No config file could be resolved.")
		return false

	var cfg := ConfigFile.new()
	var err := cfg.load(cfg_path)
	if err != OK:
		push_error("[ServerConfig] Failed to load config: %s (error %d)" % [cfg_path, err])
		return false

	_load_from_path(cfg, cfg_path)

	# Write JSON mirror
	save_json()

	_loaded = true
	config_loaded.emit()
	print("[ServerConfig] Loaded config: %s (%d keys)" % [cfg_path, cfg.get_sections().size() * 8])
	return true


## Path resolution order:
##   1. SERVER_CFG env var (absolute path)
##   2. user://server_config.cfg (writable operator override)
##   3. res://config/default_server_config.cfg (bundled default)
func _resolve_source_path() -> String:
	# Env var override
	if OS.has_environment("SERVER_CFG"):
		var env_path := OS.get_environment("SERVER_CFG")
		if FileAccess.file_exists(env_path):
			_config_path_resolved = env_path
			_json_path_resolved = env_path.get_basename() + ".json"
			return env_path
		else:
			push_warning("[ServerConfig] SERVER_CFG path does not exist: %s — falling back" % env_path)

	# User override
	var user_path := ProjectSettings.globalize_path("user://server_config.cfg")
	if FileAccess.file_exists(user_path):
		_config_path_resolved = user_path
		_json_path_resolved = user_path.get_basename() + ".json"
		return user_path

	# Bundled default — convert to absolute path for JSON mirror
	var res_path := ProjectSettings.globalize_path("res://config/default_server_config.cfg")
	_config_path_resolved = res_path
	_json_path_resolved = res_path.get_basename() + ".json"

	# First run: copy bundled default to user:// so operators can edit it
	var user_dir := ProjectSettings.globalize_path("user://")
	var copy_path := user_dir.path_join("server_config.cfg")
	if not FileAccess.file_exists(copy_path):
		var src := FileAccess.open(res_path, FileAccess.READ)
		if src:
			var dst := FileAccess.open(copy_path, FileAccess.WRITE)
			if dst:
				dst.store_string(src.get_as_text())
				dst.close()
				print("[ServerConfig] First run — copied default config to: %s" % copy_path)
				_config_path_resolved = copy_path
				_json_path_resolved = copy_path.get_basename() + ".json"
				return copy_path
			src.close()

	return res_path


func _load_from_path(cfg: ConfigFile, path: String) -> void:
	_config_path_resolved = path
	_json_path_resolved = path.get_basename() + ".json"

	# [server]
	server_name = cfg.get_value("server", "server_name", server_name)
	description = cfg.get_value("server", "description", description)
	bind_ip = cfg.get_value("server", "bind_ip", bind_ip)
	port = cfg.get_value("server", "port", port)
	max_players = cfg.get_value("server", "max_players", max_players)
	password = cfg.get_value("server", "password", password)
	tick_rate = cfg.get_value("server", "tick_rate", tick_rate)

	# [game]
	round_time_seconds = cfg.get_value("game", "round_time_seconds", round_time_seconds)
	warmup_time_seconds = cfg.get_value("game", "warmup_time_seconds", warmup_time_seconds)
	friendly_fire = cfg.get_value("game", "friendly_fire", friendly_fire)
	ff_damage_multiplier = cfg.get_value("game", "ff_damage_multiplier", ff_damage_multiplier)
	gravity = cfg.get_value("game", "gravity", gravity)
	respawn_time_seconds = cfg.get_value("game", "respawn_time_seconds", respawn_time_seconds)
	spectate_enabled = cfg.get_value("game", "spectate_enabled", spectate_enabled)

	# [movement]
	movement_walk_speed = cfg.get_value("movement", "walk_speed", movement_walk_speed)
	movement_sprint_speed = cfg.get_value("movement", "sprint_speed", movement_sprint_speed)
	movement_crouch_speed = cfg.get_value("movement", "crouch_speed", movement_crouch_speed)
	movement_jump_velocity = cfg.get_value("movement", "jump_velocity", movement_jump_velocity)
	movement_acceleration = cfg.get_value("movement", "acceleration", movement_acceleration)
	movement_air_acceleration = cfg.get_value("movement", "air_acceleration", movement_air_acceleration)
	movement_friction = cfg.get_value("movement", "friction", movement_friction)

	# [match]
	win_limit = cfg.get_value("match", "win_limit", win_limit)
	time_limit_seconds = cfg.get_value("match", "time_limit_seconds", time_limit_seconds)
	map_rotation_mode = cfg.get_value("match", "map_rotation_mode", map_rotation_mode)
	maps = cfg.get_value("match", "maps", maps)

	# Parse map list
	map_list.clear()
	var raw := maps.strip_edges()
	if not raw.is_empty():
		for m in raw.split(","):
			var trimmed := m.strip_edges()
			if not trimmed.is_empty():
				map_list.append(trimmed)

	# [rcon]
	rcon_enabled = cfg.get_value("rcon", "enabled", rcon_enabled)
	rcon_password = cfg.get_value("rcon", "password", rcon_password)
	rcon_port = cfg.get_value("rcon", "port", rcon_port)

	# [logging]
	log_level = cfg.get_value("logging", "log_level", log_level)
	log_file = cfg.get_value("logging", "log_file", log_file)

	# [teams]
	team_count = cfg.get_value("teams", "team_count", team_count)
	players_per_team = cfg.get_value("teams", "players_per_team", players_per_team)

	# Validation
	port = clampi(port, 1024, 65535)
	max_players = clampi(max_players, 1, 64)
	tick_rate = clampi(tick_rate, 30, 1000)
	team_count = clampi(team_count, 1, 8)
	players_per_team = clampi(players_per_team, 1, 32)


# ---------------------------------------------------------------------------
# Internal: Serialization
# ---------------------------------------------------------------------------

func _build_json_dict() -> Dictionary:
	return {
		"server": {
			"server_name": server_name,
			"description": description,
			"bind_ip": bind_ip,
			"port": port,
			"max_players": max_players,
			"password": "",  # intentionally omitted from JSON mirror
			"tick_rate": tick_rate,
		},
		"game": {
			"round_time_seconds": round_time_seconds,
			"warmup_time_seconds": warmup_time_seconds,
			"friendly_fire": friendly_fire,
			"ff_damage_multiplier": ff_damage_multiplier,
			"gravity": gravity,
			"respawn_time_seconds": respawn_time_seconds,
			"spectate_enabled": spectate_enabled,
		},
		"movement": {
			"walk_speed": movement_walk_speed,
			"sprint_speed": movement_sprint_speed,
			"crouch_speed": movement_crouch_speed,
			"jump_velocity": movement_jump_velocity,
			"acceleration": movement_acceleration,
			"air_acceleration": movement_air_acceleration,
			"friction": movement_friction,
		},
		"match": {
			"win_limit": win_limit,
			"time_limit_seconds": time_limit_seconds,
			"map_rotation_mode": map_rotation_mode,
			"maps": maps,
		},
		"rcon": {
			"enabled": rcon_enabled,
			"password": "",  # intentionally omitted from JSON mirror
			"port": rcon_port,
		},
		"logging": {
			"log_level": log_level,
			"log_file": log_file,
		},
		"teams": {
			"team_count": team_count,
			"players_per_team": players_per_team,
		},
	}


## Populate a ConfigFile object from the current property values.
## Used by save_cfg() to serialise all sections back to disk.
func _populate_cfg(cfg: ConfigFile) -> void:
	# [server]
	cfg.set_value("server", "server_name", server_name)
	cfg.set_value("server", "description", description)
	cfg.set_value("server", "bind_ip", bind_ip)
	cfg.set_value("server", "port", port)
	cfg.set_value("server", "max_players", max_players)
	cfg.set_value("server", "password", password)
	cfg.set_value("server", "tick_rate", tick_rate)

	# [game]
	cfg.set_value("game", "round_time_seconds", round_time_seconds)
	cfg.set_value("game", "warmup_time_seconds", warmup_time_seconds)
	cfg.set_value("game", "friendly_fire", friendly_fire)
	cfg.set_value("game", "ff_damage_multiplier", ff_damage_multiplier)
	cfg.set_value("game", "gravity", gravity)
	cfg.set_value("game", "respawn_time_seconds", respawn_time_seconds)
	cfg.set_value("game", "spectate_enabled", spectate_enabled)

	# [movement]
	cfg.set_value("movement", "walk_speed", movement_walk_speed)
	cfg.set_value("movement", "sprint_speed", movement_sprint_speed)
	cfg.set_value("movement", "crouch_speed", movement_crouch_speed)
	cfg.set_value("movement", "jump_velocity", movement_jump_velocity)
	cfg.set_value("movement", "acceleration", movement_acceleration)
	cfg.set_value("movement", "air_acceleration", movement_air_acceleration)
	cfg.set_value("movement", "friction", movement_friction)

	# [match]
	cfg.set_value("match", "win_limit", win_limit)
	cfg.set_value("match", "time_limit_seconds", time_limit_seconds)
	cfg.set_value("match", "map_rotation_mode", map_rotation_mode)
	cfg.set_value("match", "maps", maps)

	# [rcon]
	cfg.set_value("rcon", "enabled", rcon_enabled)
	cfg.set_value("rcon", "password", rcon_password)
	cfg.set_value("rcon", "port", rcon_port)

	# [logging]
	cfg.set_value("logging", "log_level", log_level)
	cfg.set_value("logging", "log_file", log_file)

	# [teams]
	cfg.set_value("teams", "team_count", team_count)
	cfg.set_value("teams", "players_per_team", players_per_team)


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

func get_config_path() -> String:
	return _config_path_resolved

func get_json_path() -> String:
	return _json_path_resolved
