## BombObjective — Server-authoritative bomb plant/defuse logic.
##
## Manages the bomb lifecycle for competitive search & destroy rounds.
## Owned by GameServer; wired to RoundManager for round-end outcomes.
##
## Bomb state machine:
##   IDLE → PLANTED → EXPLODED
##                → DEFUSED
##
## Usage (GameServer._ready()):
##   var bomb = BombObjective.new()
##   add_child(bomb)
##   bomb.round_manager = round_manager_ref
##   bomb.team_manager = team_manager_ref
##   bomb.damage_processor = damage_processor_ref
##   bomb.entity_to_peer = entity_to_peer_ref
##   bomb.peer_to_entity = peer_to_entity_ref
##   bomb.register_bomb_sites(get_tree().get_nodes_in_group("bomb_sites"))
##   round_manager.round_ended.connect(bomb.reset)
##
## Bomb sites are Area3D nodes with group "bomb_sites" in the map scene.
## Each bombsite should have a Marker3D child named "PlantPosition".

extends Node
class_name BombObjective

# Preload for headless compat
const _td = preload("res://scripts/teams/team_data.gd")

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

## Emitted when the bomb is planted by a T player.
signal bomb_planted(player_id: int, position: Vector3)

## Emitted when the bomb is defused by a CT player.
signal bomb_defused(player_id: int)

## Emitted when the bomb explodes.
signal bomb_exploded(position: Vector3)

## Emitted when a CT player starts defusing.
signal defuse_started(player_id: int)

## Emitted when a CT player cancels defuse (moves, killed, or interrupted).
signal defuse_cancelled(player_id: int)

# ---------------------------------------------------------------------------
# Bomb State enum
# ---------------------------------------------------------------------------

enum BombState {
	IDLE = 0,      # No bomb active
	PLANTED = 1,   # Bomb is planted, timer counting down
	EXPLODED = 2,  # Bomb has exploded
	DEFUSED = 3,   # Bomb was successfully defused
}

# ---------------------------------------------------------------------------
# Constants — defaults
# ---------------------------------------------------------------------------

## Default bomb timer duration in seconds (from plant to explosion).
const DEFAULT_BOMB_TIMER: float = 40.0

## Default defuse time in seconds.
const DEFAULT_DEFUSE_TIME: float = 5.0

## Default lethal radius in Godot units (full damage).
const DEFAULT_LETHAL_RADIUS: float = 10.0

## Default half-damage radius (50% damage).
const DEFAULT_HALF_RADIUS: float = 15.0

## Default explosion damage at center.
const DEFAULT_EXPLOSION_DAMAGE: float = 500.0

# ---------------------------------------------------------------------------
# Dependencies (set by GameServer before use)
# ---------------------------------------------------------------------------

## Reference to the RoundManager node.
var round_manager: RoundManager = null

## Reference to the TeamManager for checking player teams.
var team_manager: TeamManager = null

## Reference to the DamageProcessor for applying explosion damage.
var damage_processor: DamageProcessor = null

## Mapping: entity_id → peer_id (from GameServer).
var entity_to_peer: Dictionary = {}

## Mapping: peer_id → entity_id (from GameServer).
var peer_to_entity: Dictionary = {}

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

## Seconds from plant to explosion.
var bomb_timer_duration: float = DEFAULT_BOMB_TIMER

## Seconds required to defuse.
var defuse_time: float = DEFAULT_DEFUSE_TIME

## Radius within which explosion deals lethal (full) damage.
var lethal_radius: float = DEFAULT_LETHAL_RADIUS

## Radius within which explosion deals half damage.
var half_radius: float = DEFAULT_HALF_RADIUS

## Base explosion damage dealt at center (scales with distance).
var explosion_damage: float = DEFAULT_EXPLOSION_DAMAGE

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

## Current bomb state.
var _state: BombState = BombState.IDLE

## The entity ID (peer ID) of the player who planted the bomb.
var _planter_id: int = -1

## World position where the bomb is planted.
var _bomb_position: Vector3 = Vector3.ZERO

## Remaining time on the bomb timer (counts down when PLANTED).
var _bomb_timer: float = 0.0

## The entity ID (peer ID) of the player currently defusing.
var _defuser_id: int = -1

## Time remaining for the current defuse attempt.
var _defuse_timer: float = 0.0

## Registered bomb sites: Array of Area3D nodes with "bomb_sites" group.
var _bomb_sites: Array[Area3D] = []

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

func _ready() -> void:
	_state = BombState.IDLE
	print("[BombObjective] Initialised — state: IDLE")

func _process(delta: float) -> void:
	match _state:
		BombState.PLANTED:
			# Count down bomb timer
			_bomb_timer -= delta
			if _bomb_timer <= 0.0:
				_bomb_timer = 0.0
				bomb_explode()

			# Count down defuse timer if defusing
			if _defuser_id >= 0:
				_defuse_timer -= delta
				if _defuse_timer <= 0.0:
					_defuse_timer = 0.0
					_complete_defuse()

		BombState.IDLE, BombState.EXPLODED, BombState.DEFUSED:
			pass

# ---------------------------------------------------------------------------
# Public API — Site registration
# ---------------------------------------------------------------------------

## Register bomb site Area3D nodes. These are typically found via
## get_tree().get_nodes_in_group("bomb_sites").
## Each bombsite should have a Marker3D child named "PlantPosition".
func register_bomb_sites(sites: Array[Area3D]) -> void:
	_bomb_sites = sites
	print("[BombObjective] Registered %d bomb site(s)" % sites.size())

## Get all registered bomb site areas.
func get_bomb_sites() -> Array[Area3D]:
	return _bomb_sites.duplicate()

# ---------------------------------------------------------------------------
# Public API — Bomb actions
# ---------------------------------------------------------------------------

## Plant the bomb at the given world position.
## Only allowed for T-side players during the LIVE phase.
## Returns true if the bomb was planted, false otherwise.
func plant_bomb(player_id: int, position: Vector3) -> bool:
	if _state != BombState.IDLE:
		push_warning("[BombObjective] Cannot plant — bomb state is %d" % _state)
		return false

	if not _is_round_live():
		push_warning("[BombObjective] Cannot plant — round is not LIVE")
		return false

	if not _is_player_terrorist(player_id):
		push_warning("[BombObjective] Cannot plant — player %d is not on T side" % player_id)
		return false

	if not _is_player_in_bombsite(player_id):
		push_warning("[BombObjective] Cannot plant — player %d is not in a bombsite" % player_id)
		return false

	_state = BombState.PLANTED
	_planter_id = player_id
	_bomb_position = position
	_bomb_timer = bomb_timer_duration
	_defuser_id = -1
	_defuse_timer = 0.0

	print("[BombObjective] Bomb planted by player %d at (%.1f, %.1f, %.1f) — % .1fs timer" %
		[player_id, position.x, position.y, position.z, _bomb_timer])

	bomb_planted.emit(player_id, position)
	return true

## Start defusing the bomb (CT-side only, must be near the bomb).
## Returns true if defuse started, false otherwise.
func defuse_bomb(player_id: int) -> bool:
	if _state != BombState.PLANTED:
		push_warning("[BombObjective] Cannot defuse — bomb state is %d" % _state)
		return false

	if not _is_player_counter_terrorist(player_id):
		push_warning("[BombObjective] Cannot defuse — player %d is not on CT side" % player_id)
		return false

	if not _is_player_near_bomb(player_id):
		push_warning("[BombObjective] Cannot defuse — player %d is not near the bomb" % player_id)
		return false

	if _defuser_id >= 0:
		push_warning("[BombObjective] Cannot defuse — someone (%d) is already defusing" % _defuser_id)
		return false

	_defuser_id = player_id
	_defuse_timer = defuse_time

	print("[BombObjective] Defuse started by player %d — % .1fs" % [player_id, _defuse_timer])
	defuse_started.emit(player_id)
	return true

## Cancel an active defuse attempt (player moved, was killed, or interrupted).
func cancel_defuse(player_id: int) -> bool:
	if _defuser_id != player_id:
		return false

	_defuser_id = -1
	_defuse_timer = 0.0

	print("[BombObjective] Defuse cancelled by player %d" % player_id)
	defuse_cancelled.emit(player_id)
	return true

## Force the bomb to explode immediately. Called when the timer reaches zero.
func bomb_explode() -> void:
	if _state != BombState.PLANTED:
		return

	_state = BombState.EXPLODED
	_defuser_id = -1
	_defuse_timer = 0.0

	print("[BombObjective] Bomb EXPLODED at (%.1f, %.1f, %.1f)" %
		[_bomb_position.x, _bomb_position.y, _bomb_position.z])

	# Apply explosion damage to all players within radius
	_apply_explosion_damage()

	bomb_exploded.emit(_bomb_position)
	# (RoundManager notification is handled by GameServer via signal connection)

# ---------------------------------------------------------------------------
# Public API — Query
# ---------------------------------------------------------------------------

## Check if the bomb is currently planted and active.
func is_bomb_planted() -> bool:
	return _state == BombState.PLANTED

## Get the world position of the planted bomb.
## Returns Vector3.ZERO if not planted.
func get_bomb_position() -> Vector3:
	return _bomb_position

## Get the remaining time on the bomb timer (seconds).
## Returns 0.0 if not planted.
func get_bomb_timer() -> float:
	return max(0.0, _bomb_timer)

## Get the current bomb state.
func get_state() -> BombState:
	return _state

## Get the player ID of the planter.
## Returns -1 if no bomb is planted.
func get_planter_id() -> int:
	return _planter_id

## Get the player ID of the current defuser.
## Returns -1 if no one is defusing.
func get_defuser_id() -> int:
	return _defuser_id

## Get the remaining defuse time (seconds).
## Returns 0.0 if no one is defusing.
func get_defuse_timer() -> float:
	return max(0.0, _defuse_timer)

## Get the progress of the current defuse (0.0 to 1.0).
## Returns 0.0 if no one is defusing.
func get_defuse_progress() -> float:
	if _defuser_id < 0 or defuse_time <= 0.0:
		return 0.0
	return clamp(1.0 - (_defuse_timer / defuse_time), 0.0, 1.0)

## Check if a specific player is currently defusing the bomb.
func is_player_defusing(player_id: int) -> bool:
	return _defuser_id == player_id

## Check if a specific player is near the bomb (within defuse radius).
func is_player_near_bomb(player_id: int, distance_threshold: float = 3.0) -> bool:
	if _state != BombState.PLANTED:
		return false
	return _is_player_near_bomb(player_id, distance_threshold)

## Check if a specific player is inside any bombsite.
func is_player_in_bombsite(player_id: int) -> bool:
	return _is_player_in_bombsite(player_id)

# ---------------------------------------------------------------------------
# Public API — Reset
# ---------------------------------------------------------------------------

## Reset the bomb to IDLE state (called on round end / new round).
func reset() -> void:
	_state = BombState.IDLE
	_planter_id = -1
	_bomb_position = Vector3.ZERO
	_bomb_timer = 0.0
	_defuser_id = -1
	_defuse_timer = 0.0
	print("[BombObjective] Reset — state: IDLE")

# ---------------------------------------------------------------------------
# Internal — Explosion damage
# ---------------------------------------------------------------------------

## Apply damage to all players within the explosion radius.
## Deals lethal_radius damage within lethal_radius, and linear falloff
## from lethal_radius to half_radius (half damage at outer edge).
func _apply_explosion_damage() -> void:
	if damage_processor == null:
		print("[BombObjective] No damage processor — explosion deals no damage")
		return

	if entity_to_peer.is_empty() or peer_to_entity.is_empty():
		return

	# Iterate over all tracked entity IDs (from GameServer.entity_to_peer)
	for entity_id in entity_to_peer.keys():
		var health: float = damage_processor.get_health(entity_id)
		if health <= 0.0:
			continue  # Already dead or not registered

		# Get the player's position via the registered node
		var player_pos: Vector3 = _get_entity_position(entity_id)

		var distance: float = _bomb_position.distance_to(player_pos)

		# Determine damage multiplier based on distance
		var damage_mult: float = 0.0
		if distance <= lethal_radius:
			damage_mult = 1.0  # Full damage
		elif distance <= half_radius:
			# Linear falloff from full to half damage
			var t: float = (distance - lethal_radius) / (half_radius - lethal_radius) if half_radius > lethal_radius else 1.0
			damage_mult = 1.0 - (t * 0.5)  # 1.0 → 0.5
		else:
			continue  # Outside damage range

		var actual_damage: float = explosion_damage * damage_mult

		# Apply damage via DamageProcessor
		# Use the planter's entity_id as the shooter for attribution
		var planter_entity: int = peer_to_entity.get(_planter_id, -1)
		var result: Dictionary = {"hit": true, "target_id": entity_id, "damage": actual_damage}
		damage_processor.process_hit(result, planter_entity)

		print("[BombObjective] Explosion damaged entity %d — %.1f damage (dist=%.1f, mult=%.2f)" %
			[entity_id, actual_damage, distance, damage_mult])

# ---------------------------------------------------------------------------
# Internal — Defuse completion
# ---------------------------------------------------------------------------

func _complete_defuse() -> void:
	if _state != BombState.PLANTED:
		return

	var defuser: int = _defuser_id
	_state = BombState.DEFUSED
	_defuser_id = -1
	_defuse_timer = 0.0

	print("[BombObjective] Bomb defused by player %d" % defuser)
	bomb_defused.emit(defuser)
	# (RoundManager notification is handled by GameServer via signal connection)

# ---------------------------------------------------------------------------
# Internal — Helpers
# ---------------------------------------------------------------------------

## Check if the current round phase is LIVE.
func _is_round_live() -> bool:
	if round_manager == null:
		return false
	return round_manager.get_phase() == RoundManager.RoundPhase.LIVE

## Check if a player (by peer ID) is on the Terrorist team.
func _is_player_terrorist(player_id: int) -> bool:
	if team_manager == null:
		return false
	return team_manager.get_player_team(player_id) == _td.Team.TERRORIST

## Check if a player (by peer ID) is on the Counter-Terrorist team.
func _is_player_counter_terrorist(player_id: int) -> bool:
	if team_manager == null:
		return false
	return team_manager.get_player_team(player_id) == _td.Team.COUNTER_TERRORIST

## Check if a player is inside any registered bombsite.
## Iterates bomb sites and checks if the player's body overlaps.
func _is_player_in_bombsite(player_id: int) -> bool:
	if _bomb_sites.is_empty():
		return false

	var entity_id: int = peer_to_entity.get(player_id, -1)
	if entity_id < 0:
		return false

	# Get the player node from LagCompensation or scene tree
	var player_node: Node3D = _get_entity_node(entity_id)
	if player_node == null:
		return false

	for site in _bomb_sites:
		if site == null:
			continue
		# Check if the player's global position is within the site's Area3D bounds
		var site_pos: Vector3 = site.global_position
		var site_shape_extents: Vector3 = _get_area_extents(site)
		var player_pos: Vector3 = player_node.global_position

		# Simple AABB check using the Area3D's extents
		var half_size: Vector3 = site_shape_extents * 0.5
		var min_bounds: Vector3 = site_pos - half_size
		var max_bounds: Vector3 = site_pos + half_size

		if player_pos.x >= min_bounds.x and player_pos.x <= max_bounds.x and \
		   player_pos.y >= min_bounds.y and player_pos.y <= max_bounds.y and \
		   player_pos.z >= min_bounds.z and player_pos.z <= max_bounds.z:
			return true

	return false

## Check if a player is near the planted bomb (within defuse range).
func _is_player_near_bomb(player_id: int, distance_threshold: float = 3.0) -> bool:
	if _state != BombState.PLANTED:
		return false

	var entity_id: int = peer_to_entity.get(player_id, -1)
	if entity_id < 0:
		return false

	var player_node: Node3D = _get_entity_node(entity_id)
	if player_node == null:
		return false

	var distance: float = player_node.global_position.distance_to(_bomb_position)
	return distance <= distance_threshold

## Get a player's world position via their registered node.
## Tries LagCompensation first, then direct node lookup.
func _get_entity_position(entity_id: int) -> Vector3:
	var node: Node3D = _get_entity_node(entity_id)
	if node:
		return node.global_position
	return Vector3.ZERO

## Get the Node3D for a given entity_id.
## Tries to find via the scene tree or GameServer's entity→peer mapping.
func _get_entity_node(entity_id: int) -> Node3D:
	# Fallback: search the scene tree for the entity (Player nodes)
	var tree: SceneTree = get_tree()
	if tree == null:
		return null

	# Iterate all Player nodes in the tree (tagged with "players" group)
	var players: Array[Node] = tree.get_nodes_in_group("players")
	for p in players:
		if p is Node3D and p.has_method(&"get_entity_id"):
			if p.get_entity_id() == entity_id:
				return p

	# Last resort: try to find by matching peer_id via entity_to_peer
	var peer_id: int = entity_to_peer.get(entity_id, -1)
	if peer_id >= 0:
		for p in players:
			if p is Node3D and p.has_method(&"get_peer_id"):
				if p.get_peer_id() == peer_id:
					return p

	return null

## Get the approximate extents of an Area3D from its collision shapes.
func _get_area_extents(area: Area3D) -> Vector3:
	var extents: Vector3 = Vector3(4.0, 2.0, 2.0)  # Default fallback (8×4×4)

	for child in area.get_children():
		if child is CollisionShape3D and child.shape:
			if child.shape is BoxShape3D:
				extents = child.shape.size
				break
			elif child.shape is SphereShape3D:
				var r: float = child.shape.radius
				extents = Vector3(r * 2.0, r * 2.0, r * 2.0)
				break

	return extents
