class_name BotAI
extends RefCounted

# "Seek and shoot" bot AI (see overview/bots.md), personality-driven by
# BotPersonality (bots/bot_personality.gd, rolled once per bot-slot by
# BotManager). Five independent traits reshape the same base behavior instead
# of branching into separate hand-written behavior trees per archetype:
#   aggression -> how close it presses in before it's satisfied with range
#   caution    -> health fraction at which it breaks off and flees
#   accuracy   -> how tightly it tracks the target's true position
#   reaction   -> how often it refreshes its aim point (lag on movers)
#   awareness  -> detection radius for picking a target at all
# Kept isolated from ship_movement.gd's networking code entirely so future
# tuning only ever touches this file and game_config.gd's bot_* ranges.

var _ship: Ship
var _race_id: int
var personality: BotPersonality

var _stop_distance: float
var _retreat_health_frac: float
var _aim_tolerance_rad: float
var _reaction_interval: float
var _awareness_range_sq: float

var _current_target: Ship = null
var _tracked_pos: Vector2 = Vector2.ZERO
var _reaction_timer: float = 0.0


func _init(ship: Ship, race_id: int, bot_personality: BotPersonality = null) -> void:
	_ship = ship
	_race_id = race_id
	personality = bot_personality if bot_personality != null else BotPersonality.random()

	_stop_distance = lerp(GameConfig.bot_stop_distance_max, GameConfig.bot_stop_distance_min, personality.aggression)
	_retreat_health_frac = lerp(GameConfig.bot_retreat_health_frac_min, GameConfig.bot_retreat_health_frac_max, personality.caution)
	_aim_tolerance_rad = deg_to_rad(lerp(GameConfig.bot_aim_tolerance_worst_deg, GameConfig.bot_aim_tolerance_best_deg, personality.accuracy))
	_reaction_interval = lerp(GameConfig.bot_reaction_update_worst_sec, GameConfig.bot_reaction_update_best_sec, personality.reaction)
	var awareness_range: float = lerp(GameConfig.bot_awareness_range_min, GameConfig.bot_awareness_range_max, personality.awareness)
	_awareness_range_sq = awareness_range * awareness_range
	_reaction_timer = randf() * _reaction_interval  # desync bots so they don't all re-aim on the same tick


func compute_input(delta: float) -> Dictionary:
	var target := _find_nearest_enemy()
	if target == null:
		_current_target = null
		return _compute_advance_input()

	if target != _current_target:
		_current_target = target
		_reaction_timer = 0.0  # snap to a fresh target immediately, don't lag onto it

	_reaction_timer -= delta
	if _reaction_timer <= 0.0:
		_reaction_timer = _reaction_interval
		var jitter_radius: float = (1.0 - personality.accuracy) * GameConfig.bot_aim_jitter_max_px
		var jitter := Vector2(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0)) * jitter_radius
		_tracked_pos = target.global_position + jitter

	var fleeing: bool = float(_ship.health) / float(GameConfig.ship_max_health) < _retreat_health_frac
	var true_distance: float = _ship.global_position.distance_to(target.global_position)

	# Ship forward is Vector2.UP.rotated(rotation) (see ship_movement.gd
	# _simulate_step), so facing a point requires offsetting by the angle
	# between Vector2.UP and the zero-rotation forward vector (PI/2).
	var aim_at: Vector2 = _ship.global_position - target.global_position if fleeing else _tracked_pos - _ship.global_position
	var desired_rot: float = aim_at.angle() + PI / 2.0
	var diff: float = angle_difference(_ship.rotation, desired_rot)

	return {
		"up": fleeing or true_distance > _stop_distance,
		"down": false,
		"left": diff < -_aim_tolerance_rad,
		"right": diff > _aim_tolerance_rad,
		"shoot": not fleeing and absf(diff) <= _aim_tolerance_rad and true_distance <= GameConfig.bot_engage_range,
	}


# No enemy within awareness range yet (common right after match start: two
# 7-bot teams spawn on opposite sides of the map, farther apart than most
# personality rolls' awareness_range — see GameConfig.bot_awareness_range_min
# comment). Without this, those bots would just sit still at spawn forever,
# waiting for someone to wander into range. Advance toward the map's center
# instead, closing the distance until awareness (or GameConfig.bot_engage_range)
# picks up an enemy.
func _compute_advance_input() -> Dictionary:
	var center: Vector2 = GameConfig.world_bounds.get_center()
	var to_center: Vector2 = center - _ship.global_position
	if to_center.length_squared() < 2500.0:  # ~50px: already at the rally point
		return {"up": false, "down": false, "left": false, "right": false, "shoot": false}

	var desired_rot: float = to_center.angle() + PI / 2.0
	var diff: float = angle_difference(_ship.rotation, desired_rot)
	return {
		"up": true,
		"down": false,
		"left": diff < -_aim_tolerance_rad,
		"right": diff > _aim_tolerance_rad,
		"shoot": false,
	}


func _find_nearest_enemy() -> Ship:
	var best: Ship = null
	var best_dist := INF
	for node in _ship.get_tree().get_nodes_in_group("ships"):
		var candidate := node as Ship
		if candidate == null or candidate == _ship or not candidate.visible:
			continue
		var info: Dictionary = PlayerRegistry.get_info(candidate.peer_id)
		if info.get("race", -1) == _race_id:
			continue
		var dist: float = _ship.global_position.distance_squared_to(candidate.global_position)
		if dist > _awareness_range_sq:
			continue
		if dist < best_dist:
			best_dist = dist
			best = candidate
	return best
