extends Node

# Server-authoritative bot fill for casual matches (see overview/bots.md):
# keeps each of the match's 2 offered races at a minimum of
# GameConfig.bot_min_team_size total (humans + bots), spawning/despawning
# bots as players join and leave.
#
# All spawn/despawn logic funnels through _reconcile_race() so every future
# tweak (different fill curve, per-map team size, bot difficulty swaps,
# etc.) has one place to change instead of being scattered across
# join/leave handlers.

var _world = null  # World instance; untyped like ship_movement.gd's own
                    # world ref, since World has no class_name of its own
var _bots_by_race: Dictionary = {}  # race_id -> Array[int] (bot peer_ids)
var _next_bot_id: int = -1


func _ready() -> void:
	PlayerRegistry.loadout_updated.connect(_on_loadout_updated)
	PlayerRegistry.player_removed.connect(_on_player_removed)
	PlayerRegistry.race_changed.connect(_on_race_changed)


# Called once by World.decide_offered_races() when the match's 2 races are
# decided, to fill both teams with bots before any human has picked a race.
func on_match_start(world, offered_race_ids: Array) -> void:
	if not multiplayer.is_server():
		return
	_world = world
	_bots_by_race.clear()
	_next_bot_id = -1
	for race_id in offered_race_ids:
		_reconcile_race(race_id)


func _on_loadout_updated(peer_id: int) -> void:
	if not multiplayer.is_server() or _world == null or peer_id < 0:
		return
	_reconcile_race(PlayerRegistry.get_info(peer_id).get("race", 0))


func _on_player_removed(peer_id: int, info: Dictionary) -> void:
	if not multiplayer.is_server() or _world == null or peer_id < 0:
		return
	_reconcile_race(info.get("race", 0))


# A live team swap (pause menu SELECT TEAM) fires loadout_updated for the
# *new* race, but that alone leaves the race the player just left one human
# short — this backfills it. _on_loadout_updated handles the new race.
func _on_race_changed(peer_id: int, old_race: int, _new_race: int) -> void:
	if not multiplayer.is_server() or _world == null or peer_id < 0:
		return
	_reconcile_race(old_race)


func _reconcile_race(race_id: int) -> void:
	if race_id <= 0:
		return
	var humans := _count_humans(race_id)
	var desired: int = max(0, GameConfig.bot_min_team_size - humans)
	var current: Array = _bots_by_race.get(race_id, [])
	while current.size() < desired:
		current.append(_spawn_bot(race_id, current.size()))
	while current.size() > desired:
		_despawn_bot(current.pop_back())
	_bots_by_race[race_id] = current


func _count_humans(race_id: int) -> int:
	var count := 0
	for peer_id in PlayerRegistry.players:
		if peer_id > 0 and PlayerRegistry.players[peer_id].race == race_id:
			count += 1
	return count


func _spawn_bot(race_id: int, slot_index: int) -> int:
	var bot_id := _next_bot_id
	_next_bot_id -= 1

	var race := _find_race(race_id)
	var ship_def: Dictionary = _bot_ship_def(race, slot_index)
	PlayerRegistry.register_bot(
		bot_id, BotNames.random_name(), race_id,
		ship_def.path, ship_def.scale, ship_def.speed_factor, ship_def.role,
		ship_def.get("flame_path", ""), ship_def.get("sound_path", "")
	)

	var ship_node := _world.spawn_peer(bot_id) as Ship
	if ship_node:
		ship_node.bot_ai = BotAI.new(ship_node, race_id)
	return bot_id


# Bots used to always fly the race's Fighter (ships[0]); now the first-spawned
# bot on a team flies its Tank, the next two fly its Gunner, and every bot
# after that flies its Fighter -- so a fully bot-filled team always has at
# least 1 Tank + 2 Gunner "backbone" instead of reading as an all-Fighter
# swarm. Keyed off slot_index (this bot's position within its race's bot
# array, before it's appended -- see _reconcile_race()) rather than the
# team's current size, so growing/shrinking the roster never reshuffles an
# already-spawned bot's ship: _reconcile_race() only ever adds at the tail
# and removes from the tail (pop_back()), so the Tank/Gunner bots spawned
# into the earliest slots are always the last ones cut as humans join.
# Relies on team_select.gd's RACES ship order being [Fighter, Gunner, Tank]
# for every faction.
func _bot_ship_def(race: Dictionary, slot_index: int) -> Dictionary:
	if slot_index == 0:
		return race.ships[2]  # Tank
	elif slot_index <= 2:
		return race.ships[1]  # Gunner
	else:
		return race.ships[0]  # Fighter


func _despawn_bot(bot_id: int) -> void:
	_world.despawn_peer(bot_id)
	PlayerRegistry.unregister_bot(bot_id)


func _find_race(race_id: int) -> Dictionary:
	for race in TeamSelect.RACES:
		if race.id == race_id:
			return race
	return TeamSelect.RACES[0]
