extends CharacterBody3D

signal arrived_at_target(sim_id: int)
signal navigation_failed(sim_id: int)
signal position_changed(sim_id: int, active_position: Vector3)

const DEBUG_LOGS := true

@export var move_speed := 3.0
@export var rotation_speed := 8.0
@export var waypoint_reached_distance := 0.35
@export var stuck_timeout := 1.5

@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
@onready var carried_food_visual: MeshInstance3D = $CarriedFood

var has_reported_arrival := false
var sim_id: int = -1
var npc_name: String = ""
var profession: StringName

var current_path: PackedVector3Array = []
var path_index := 0
var current_target := Vector3.INF
var arrival_distance := 0.6
var stuck_time := 0.0
var path_request_id := 0
var path_pending := false

var is_dead_visual := false
var dead_material := StandardMaterial3D.new()


func debug_log(message: String) -> void:
	if DEBUG_LOGS:
		print("[NPCVisual] ", name, " | ", message)


func _ready() -> void:
	dead_material.albedo_color = Color(0.25, 0.25, 0.25, 1.0)


func setup_from_sim(npc: SimNPC) -> void:
	sim_id = npc.id
	npc_name = npc.npc_name
	profession = npc.profession
	name = "NPC_%s_%s" % [sim_id, npc_name]
	set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)


func set_carried_food_visible(is_visible: bool) -> void:
	carried_food_visual.visible = is_visible and not is_dead_visual


func set_target_position(pos: Vector3) -> void:
	path_request_id += 1
	var request_id := path_request_id
	current_target = pos
	has_reported_arrival = false
	stuck_time = 0.0
	path_pending = false

	if global_position.distance_to(pos) <= arrival_distance:
		current_path = PackedVector3Array()
		path_index = 0
		debug_log("Already close to target, will report arrival")
		return

	nav_agent.target_position = pos
	path_pending = true

	await get_tree().physics_frame

	if request_id != path_request_id or is_dead_visual:
		return

	current_path = NavigationServer3D.map_get_path(
		get_world_3d().navigation_map, global_position, pos, true
	)

	path_pending = false
	path_index = 0

	if current_path.is_empty():
		debug_log("Path is empty")
		_report_navigation_failure_once()
	else:
		debug_log("New target: %s Path points: %s" % [pos, current_path.size()])


func _physics_process(delta: float) -> void:
	if is_dead_visual:
		velocity = Vector3.ZERO
		move_and_slide()
		return

	if current_target == Vector3.INF:
		velocity = Vector3.ZERO
		move_and_slide()
		return

	if path_pending:
		velocity = Vector3.ZERO
		move_and_slide()
		return

	if global_position.distance_to(current_target) <= arrival_distance:
		_report_arrival_once()
		velocity = Vector3.ZERO
		move_and_slide()
		return

	if current_path.is_empty() or path_index >= current_path.size():
		velocity = Vector3.ZERO
		move_and_slide()
		_report_navigation_failure_once()
		return

	var next_pos := current_path[path_index]
	var flat_next_pos := Vector3(next_pos.x, global_position.y, next_pos.z)
	var to_next := flat_next_pos - global_position

	if to_next.length() <= waypoint_reached_distance:
		path_index += 1
		return

	var direction := to_next.normalized()
	var prev_pos := global_position

	velocity.x = direction.x * move_speed
	velocity.z = direction.z * move_speed
	velocity.y = 0.0

	move_and_slide()
	if not global_position.is_equal_approx(prev_pos):
		position_changed.emit(sim_id, global_position)

	var minimum_progress := move_speed * delta * 0.1
	if global_position.distance_squared_to(prev_pos) < minimum_progress * minimum_progress:
		stuck_time += delta
		if stuck_time >= stuck_timeout:
			debug_log("Movement remained blocked")
			_report_navigation_failure_once()
			return
	else:
		stuck_time = 0.0

	var target_angle := atan2(direction.x, direction.z)
	rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)


func _report_arrival_once() -> void:
	if has_reported_arrival:
		return

	has_reported_arrival = true
	current_path = PackedVector3Array()
	path_index = 0

	debug_log("Arrived at target")
	arrived_at_target.emit(sim_id)


func _report_navigation_failure_once() -> void:
	if has_reported_arrival:
		return

	has_reported_arrival = true
	current_path = PackedVector3Array()
	path_index = 0
	current_target = Vector3.INF
	path_pending = false
	velocity = Vector3.ZERO

	debug_log("Navigation failed")
	navigation_failed.emit(sim_id)


func apply_dead_visual_state() -> void:
	is_dead_visual = true
	carried_food_visual.visible = false
	path_request_id += 1
	velocity = Vector3.ZERO
	current_path = PackedVector3Array()
	path_index = 0
	current_target = Vector3.INF
	path_pending = false
	has_reported_arrival = true

	scale = Vector3(0.8, 0.25, 1.2)
	rotation_degrees.x = 90.0

	for child in get_children():
		if child is MeshInstance3D:
			child.material_override = dead_material

	collision_layer = 0
	collision_mask = 0
	debug_log("Applied dead visual state")
