## GameServer — drives the 128Hz simulation loop.
##
## Owns a SimulationServer instance, drives its tick loop in _physics_process,
## and manages weapon configuration. Acts as the bridge between the GDScript
## world (NetworkManager, ServerMain) and the C++ simulation core.
##
## Architecture:
##   GameServer (Node, autoload candidate)
##   ├── SimulationServer (GDExtension C++) — game state + hit detection
##   │   ├── applies client input via apply_input()
##   │   └── process_compensated_fire() with lag compensation rewind
##   └── WeaponManager (optional, on the player node for client-side rate limit)
##
## Usage (server_main.gd):
##   func _ready():
##       var gs = GameServer.new()
##       add_child(gs)
##       gs.configure(ServerConfig.tick_rate, ServerConfig.make_movement_dict())
##
## Standalone / listen-server test:
##   var gs = get_node("/root/GameServer")
##   if gs: gs.start_simulation()
##
extends Node

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

## Emitted after each simulation tick with hit results.
signal tick_completed(tick: int)

## Emitted when a player is damaged (for scoreboard/UI updates).
signal player_damaged(victim_entity_id: int, shooter_entity_id: int, damage: float, killed: bool)

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

var simulation_server: Object = null
var is_running: bool = false

# Map entity_id → peer_id for broadcasting damage events
var entity_to_peer: Dictionary = {}  # entity_id (int) → peer_id (int)

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

func _ready() -> void:
	# Create the SimulationServer (stub if GDExtension not compiled)
	var SimServerClass = load("res://server/scripts/simulation_server_stub.gd")
	if ClassDB.class_exists(&"SimulationServer"):
		SimServerClass = ClassDB.instantiate(&"SimulationServer").get_script()
	simulation_server = SimServerClass.new()
	simulation_server.set_tick_rate(128)

	# Apply movement config from ServerConfig singleton if available
	if ServerConfig and ServerConfig.has_method(&"make_movement_dict"):
		simulation_server.set_movement_config(ServerConfig.make_movement_dict())

	# Configure the default hitscan weapon (Assault Rifle)
	simulation_server.set_weapon_config({
		"base_damage": 30.0,
		"head_multiplier": 4.0,
		"body_multiplier": 1.0,
		"arm_multiplier": 0.75,
		"leg_multiplier": 0.6,
		"max_range": 500.0,
		"spread_degrees": 0.5,
		"fire_rate_hz": 10.0,
	})

	# Set history depth for lag compensation (64 ticks = ~500ms at 128Hz)
	simulation_server.set_history_depth(64)

	# Register as singleton so FPSCharacterController can find us
	Engine.register_singleton("SimulationServer", simulation_server)

	print("[GameServer] SimulationServer created. Tick rate: %d Hz" % simulation_server.get_tick_rate())

func _exit_tree() -> void:
	if simulation_server:
		simulation_server.stop()
		simulation_server = null

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

## Start the simulation.
func start_simulation() -> void:
	if is_running:
		return
	if simulation_server == null:
		push_error("[GameServer] SimulationServer is null, cannot start")
		return

	simulation_server.start()
	is_running = true
	print("[GameServer] Simulation started")

## Stop the simulation.
func stop_simulation() -> void:
	if not is_running:
		return
	if simulation_server:
		simulation_server.stop()
	is_running = false
	print("[GameServer] Simulation stopped")

## Spawn a player entity and map to peer ID.
## Returns the entity ID assigned by the simulation server.
func spawn_player_entity(peer_id: int, spawn_pos: Vector3) -> int:
	if simulation_server == null:
		return -1

	if not is_running:
		start_simulation()

	var entity_id: int = simulation_server.spawn_entity(spawn_pos)
	if entity_id < 0 or entity_id >= 65535:
		push_error("[GameServer] Failed to spawn entity for peer %d" % peer_id)
		return -1

	entity_to_peer[entity_id] = peer_id
	print("[GameServer] Spawned entity %d for peer %d at (%.1f, %.1f, %.1f)" % [entity_id, peer_id, spawn_pos.x, spawn_pos.y, spawn_pos.z])
	return entity_id

## Despawn a player entity.
func despawn_player_entity(entity_id: int) -> void:
	if simulation_server == null:
		return

	if entity_id >= 0:
		simulation_server.despawn_entity(entity_id)

	# Remove from mapping
	if entity_id in entity_to_peer:
		entity_to_peer.erase(entity_id)

## Get the peer ID for a given entity ID.
func get_peer_for_entity(entity_id: int) -> int:
	return entity_to_peer.get(entity_id, -1)

## Get the underlying SimulationServer reference.
func get_simulation_server() -> Object:
	return simulation_server

## Configure weapon damage profile.
func set_weapon(config: Dictionary) -> void:
	if simulation_server:
		simulation_server.set_weapon_config(config)

# ---------------------------------------------------------------------------
# Main loop (128 Hz)
# ---------------------------------------------------------------------------

func _physics_process(delta: float) -> void:
	if not is_running or simulation_server == null:
		return

	# Drive the fixed-timestep simulation loop
	while simulation_server.can_tick(delta):
		var snapshot: PackedByteArray = simulation_server.tick()
		# snapshot is the serialized state — send to network layer in Phase 2
		# For now, just emit the tick completed signal
		tick_completed.emit(simulation_server.get_stats().get("tick_count", 0))

		# Check for hit results and emit damage events
		var hit_result: Dictionary = simulation_server.get_last_hit_result()
		if hit_result.get("hit", false):
			var victim_id: int = hit_result.get("entity_id", -1)
			var damage: float = hit_result.get("damage", 0.0)
			var killed: bool = hit_result.get("killed", false)
			# For now we don't know the shooter — this will be wired in Phase 2
			player_damaged.emit(victim_id, -1, damage, killed)
