class_name DedicatedServerConfig
extends RefCounted

const DEFAULT_NAME: String = "NETfishing Dedicated Server"
const DEFAULT_BIND_ADDRESS: String = "*"
const DEFAULT_PORT: int = 7777
const DEFAULT_MAX_PLAYERS: int = 8

var server_name: String = DEFAULT_NAME
var bind_address: String = DEFAULT_BIND_ADDRESS
var port: int = DEFAULT_PORT
var max_players: int = DEFAULT_MAX_PLAYERS
var public_listing: bool = false
var discovery_url: String = ""
var data_directory: String = ""
var error_message: String = ""


static func from_runtime() -> DedicatedServerConfig:
	var result := DedicatedServerConfig.new()
	result.data_directory = ProjectSettings.globalize_path(
		"user://dedicated-server-data"
	)
	result.discovery_url = str(ProjectSettings.get_setting(
		"network/discovery/base_url", ""
	)).strip_edges()
	var config_path: String = OS.get_environment(
		"NETFISHING_SERVER_CONFIG"
	).strip_edges()
	for argument: String in OS.get_cmdline_user_args():
		if argument.begins_with("--config="):
			config_path = argument.trim_prefix("--config=").strip_edges()
	if not config_path.is_empty() and not result._load_file(config_path):
		return result
	result._apply_environment()
	result._apply_arguments(OS.get_cmdline_user_args())
	result._validate()
	return result


func is_valid() -> bool:
	return error_message.is_empty()


func _load_file(path: String) -> bool:
	var file := ConfigFile.new()
	var error: Error = file.load(path)
	if error != OK:
		error_message = "Could not load server configuration: %s" % path
		return false
	server_name = str(file.get_value("server", "name", server_name))
	bind_address = str(file.get_value(
		"server", "bind_address", bind_address
	))
	port = int(file.get_value("server", "port", port))
	max_players = int(file.get_value(
		"server", "max_players", max_players
	))
	public_listing = bool(file.get_value(
		"server", "public", public_listing
	))
	data_directory = str(file.get_value(
		"server", "data_directory", data_directory
	))
	discovery_url = str(file.get_value(
		"discovery", "url", discovery_url
	))
	return true


func _apply_environment() -> void:
	server_name = _environment_string(
		"NETFISHING_SERVER_NAME", server_name
	)
	bind_address = _environment_string(
		"NETFISHING_SERVER_BIND", bind_address
	)
	port = _environment_int("NETFISHING_SERVER_PORT", port)
	max_players = _environment_int(
		"NETFISHING_SERVER_MAX_PLAYERS", max_players
	)
	public_listing = _environment_bool(
		"NETFISHING_SERVER_PUBLIC", public_listing
	)
	discovery_url = _environment_string(
		"NETFISHING_DISCOVERY_URL", discovery_url
	)
	data_directory = _environment_string(
		"NETFISHING_DATA_DIR", data_directory
	)


func _apply_arguments(arguments: PackedStringArray) -> void:
	for argument: String in arguments:
		if argument.begins_with("--name="):
			server_name = argument.trim_prefix("--name=")
		elif argument.begins_with("--bind="):
			bind_address = argument.trim_prefix("--bind=")
		elif argument.begins_with("--port="):
			port = _parse_int(argument.trim_prefix("--port="), port)
		elif argument.begins_with("--max-players="):
			max_players = _parse_int(
				argument.trim_prefix("--max-players="), max_players
			)
		elif argument.begins_with("--data-dir="):
			data_directory = argument.trim_prefix("--data-dir=")
		elif argument.begins_with("--discovery-url="):
			discovery_url = argument.trim_prefix("--discovery-url=")
		elif argument == "--public":
			public_listing = true
		elif argument == "--private":
			public_listing = false


func _validate() -> void:
	server_name = server_name.strip_edges().left(48)
	bind_address = bind_address.strip_edges()
	discovery_url = discovery_url.strip_edges().trim_suffix("/")
	data_directory = data_directory.strip_edges()
	if server_name.is_empty():
		error_message = "Server name cannot be empty."
	elif not _safe_text(server_name):
		error_message = "Server name contains unsupported characters."
	elif bind_address.is_empty():
		error_message = "Bind address cannot be empty."
	elif port < 1 or port > 65535:
		error_message = "Server port must be between 1 and 65535."
	elif max_players < 1 or max_players > 128:
		error_message = "Maximum players must be between 1 and 128."
	elif not data_directory.is_empty() and not data_directory.is_absolute_path():
		error_message = "Server data directory must be an absolute path."
	elif public_listing and not (
		discovery_url.begins_with("https://")
		or discovery_url.begins_with("http://")
	):
		error_message = "A public server requires a discovery URL."


static func _safe_text(value: String) -> bool:
	for index: int in value.length():
		var codepoint: int = value.unicode_at(index)
		if codepoint < 32 or codepoint == 127:
			return false
	return true


static func _environment_string(name: String, fallback: String) -> String:
	return OS.get_environment(name) if OS.has_environment(name) else fallback


static func _environment_int(name: String, fallback: int) -> int:
	return (
		_parse_int(OS.get_environment(name), fallback)
		if OS.has_environment(name) else fallback
	)


static func _environment_bool(name: String, fallback: bool) -> bool:
	if not OS.has_environment(name):
		return fallback
	var value: String = OS.get_environment(name).strip_edges().to_lower()
	return value in ["1", "true", "yes", "on"]


static func _parse_int(value: String, fallback: int) -> int:
	return int(value) if value.strip_edges().is_valid_int() else fallback
