class_name Employee
extends Node2D

const SPEECH_BUBBLE_SCRIPT := preload("res://scripts/core/speech_bubble.gd")

# Un employé embauché. Serveur et cuisinier partagent ce script: ils marchent
# pareil et choisissent leur tâche pareil — seule la liste des tâches change.
#
# Aucune coordination centrale. Chacun choisit la tâche la mieux notée **pour
# lui**, et réserve ce qu'il vise. Deux serveurs aux profils opposés se
# répartissent donc le travail sans qu'aucun code ne les répartisse.

enum Task { NONE, TAKE_ORDER, SERVE, BUS, COOK, TAKEOUT, TAKEOUT_SERVICE }
# Une tâche qui déplace une assiette a deux temps: aller la chercher, puis la
# porter. Sans ce second temps, l'assiette se téléportait à destination.
# Un cuisinier a un troisième temps: il **tient** son four jusqu'à ce que le
# plat sorte, puis le porte. Sans ce temps-là il repartait à l'allumage et le
# plat se téléportait sur l'étagère.
enum Stage { GO, CARRY, TEND }

const BASE_SPEED := 150.0
const MIN_SERVER_PACE := 0.35
const MIN_COOK_PACE := 0.5
const ARRIVAL_DISTANCE := 14.0
const BASE_SIGHT := 420.0
const BASE_ACTION_TIME := 0.9
const BODY_RADIUS := 16.0

var employee_id := ""
var restaurant: Node
var role := "server"
var body_color := Color("6ec8a8")

var task := Task.NONE
var target_position := Vector2.ZERO
# Ce que la tâche vise. Un seul est renseigné à la fois.
var target_table := -1
var target_station := -1
var target_dish: Dish
var carried: Array[Dish] = []
var action_remaining := 0.0
var stage := Stage.GO
# L'assiette réellement en main. Le porteur d'une assiette est n'importe quel
# Node2D: le suivi visuel est le même que pour le joueur.
var held: Dish
var speech_bubble: Node2D

func setup(new_id: String, manager: Node) -> void:
	employee_id = new_id
	restaurant = manager
	var definition: Dictionary = DataCatalog.EMPLOYEES.get(new_id, {})
	role = str(definition.get("role", "server"))
	var configured: Variant = definition.get("color", Color("6ec8a8"))
	body_color = configured if configured is Color else Color(str(configured))
	_build_speech_bubble(definition.get("speech_bubbles", {}))
	say("arrive")
	queue_redraw()

func _build_speech_bubble(config: Variant) -> void:
	speech_bubble = SPEECH_BUBBLE_SCRIPT.new()
	speech_bubble.name = "SpeechBubble"
	add_child(speech_bubble)
	var safe_config: Dictionary = config if config is Dictionary else {}
	speech_bubble.call("bind_to_actor", self, safe_config, Vector2(0.0, -29.0))

func say(trigger_id: String) -> bool:
	return speech_bubble != null and is_instance_valid(speech_bubble) \
		and bool(speech_bubble.call("trigger_event", trigger_id))

func skill(name: String) -> float:
	return GameState.employee_skill(employee_id, name)

# Une compétence absente vaut zéro, donc toute vitesse part d'un plancher: un
# employé sans `pace` déclarée marche quand même.
func move_speed() -> float:
	var role_floor := MIN_COOK_PACE if role == "cook" else MIN_SERVER_PACE
	return BASE_SPEED * maxf(role_floor, skill("pace"))

func sight_range() -> float:
	return BASE_SIGHT * maxf(0.35, skill("sight"))

func action_time(skill_name: String) -> float:
	return BASE_ACTION_TIME / maxf(0.25, skill(skill_name))

# Four author-facing values choose *what* a server prefers. Skills still decide
# how well and how quickly that work is performed; they no longer double as an
# opaque priority system.
func task_preference(kind: String) -> float:
	var definition: Dictionary = DataCatalog.EMPLOYEES.get(employee_id, {})
	return clampf(float(definition.get("task_preferences", {}).get(kind, 1.0)), 0.0, 5.0)

func tray_capacity() -> int:
	return maxi(1, int(round(maxf(1.0, skill("tray")))))

# Vitesse de cuisson propre: le métier, multiplié par l'affinité du poste puis
# par celle du plat. C'est là que le barman et l'épicier se distinguent.
func cooking_rate_for(kind: String, recipe_id: String) -> float:
	var definition: Dictionary = DataCatalog.EMPLOYEES.get(employee_id, {})
	var station_bonus := float(definition.get("station_affinity", {}).get(kind, 1.0))
	var recipe_bonus := float(definition.get("recipe_affinity", {}).get(recipe_id, 1.0))
	return maxf(0.1, skill("craft")) * maxf(0.05, station_bonus) * maxf(0.05, recipe_bonus)

func _process(delta: float) -> void:
	if restaurant == null or not is_instance_valid(restaurant):
		return
	# L'objectif se vérifie à chaque frame, pas seulement à la fin du trajet.
	# Le joueur peut prendre l'assiette visée, le client peut partir: continuer
	# à marcher vers un objectif disparu est du temps perdu, et ça se voit.
	if task != Task.NONE and not _task_still_valid():
		_abandon_task()
	if task == Task.NONE:
		_choose_task()
		if task == Task.NONE:
			_idle_step(delta)
			return
		say("task_started")
	# Devant son four, on attend que le plat sorte: c'est du travail, pas du
	# déplacement, et rien d'autre ne doit se passer pendant ce temps.
	if (task == Task.COOK or task == Task.TAKEOUT or task == Task.TAKEOUT_SERVICE) and stage == Stage.TEND:
		match task:
			Task.TAKEOUT: _tend_takeout()
			Task.TAKEOUT_SERVICE: _tend_takeout_service()
			_: _tend_station()
		queue_redraw()
		return
	if action_remaining > 0.0:
		action_remaining = maxf(0.0, action_remaining - delta)
		if action_remaining <= 0.0:
			_finish_task()
		queue_redraw()
		return
	global_position = global_position.move_toward(target_position, move_speed() * delta)
	if global_position.distance_to(target_position) <= ARRIVAL_DISTANCE:
		_begin_action()
	queue_redraw()

# --- Validité de l'objectif --------------------------------------------------

# Ce que la tâche suppose encore vrai. Chaque cas dit ce qui le rendrait vain.
func _task_still_valid() -> bool:
	match task:
		Task.TAKE_ORDER:
			var guest := _guest_at_table(target_table)
			return guest != null and not GameState.board_has_line(target_table)
		Task.SERVE:
			if stage == Stage.CARRY:
				return held != null and is_instance_valid(held) and _customer_waiting_for(held.recipe_id) != null
			if target_dish == null or not is_instance_valid(target_dish):
				return false
			# Prise par quelqu'un d'autre — le joueur, ou un collègue plus rapide.
			if target_dish.state != Dish.State.FILLED or target_dish.carrier != null:
				return false
			return _customer_waiting_for(target_dish.recipe_id) != null
		Task.BUS:
			if stage == Stage.CARRY:
				return held != null and is_instance_valid(held)
			return target_dish != null and is_instance_valid(target_dish) \
				and target_dish.state == Dish.State.DIRTY and target_dish.carrier == null
		Task.COOK:
			# Porter le plat ne dépend plus ni du four ni de la commande.
			if stage == Stage.CARRY:
				return held != null and is_instance_valid(held)
			# Tenir son four: seul le poste doit encore exister.
			if stage == Stage.TEND:
				return target_station >= 0 and target_station < restaurant.stations.size()
			# En chemin, le poste compte autant que la commande: le joueur a pu
			# allumer le four pendant qu'on marchait vers lui.
			return target_station >= 0 and not restaurant.station_is_busy(target_station) \
				and not GameState.order_board_line(target_table).is_empty()
		Task.TAKEOUT:
			return restaurant.takeout_cook_is_assigned(self) and restaurant.takeout_needs_cook()
		Task.TAKEOUT_SERVICE:
			return restaurant.takeout_server_is_assigned(self) and restaurant.takeout_needs_server()
	return true

# Abandonner proprement: ce qu'on portait se repose, ce qu'on avait réservé se
# rend. Sans ça une assiette resterait accrochée à un serveur qui n'en veut plus.
func _abandon_task() -> void:
	if held != null and is_instance_valid(held):
		held.place_on(global_position)
	held = null
	# Une ligne dont la cuisson a démarré appartient au poste, plus au cuisinier:
	# la rendre la ferait recommencer par un collègue.
	if task == Task.COOK and target_table >= 0 and stage == Stage.GO:
		GameState.release_order_line(target_table)
	if task == Task.TAKEOUT:
		restaurant.release_takeout_cook(self)
	if task == Task.TAKEOUT_SERVICE:
		restaurant.release_takeout_server(self)
	_clear_task()

func _guest_at_table(table_index: int) -> Customer:
	if table_index < 0:
		return null
	for guest: Customer in restaurant.customers:
		if is_instance_valid(guest) and guest.table_index == table_index and guest.state == Customer.State.WAITING:
			return guest
	return null

# --- Choix de tâche ----------------------------------------------------------

func _choose_task() -> void:
	if role == "cook":
		_choose_cook_task()
		return
	_choose_server_task()

# Every available task has one urgency and one preference from the same 0–5
# profile. Skills affect execution only, which makes hiring choices readable.
func _choose_server_task() -> void:
	var best_score := 0.0
	var chosen := Task.NONE
	var chosen_table := -1
	var chosen_dish: Dish = null
	var chosen_position := Vector2.ZERO

	var order_target := _customer_needing_order()
	if order_target != null:
		var score := 1.0 * task_preference("take_order")
		if score > best_score:
			best_score = score
			chosen = Task.TAKE_ORDER
			chosen_table = order_target.table_index
			chosen_position = order_target.global_position

	var ready_dish := _dish_ready_to_serve()
	if ready_dish != null:
		var score := 1.2 * task_preference("serve")
		if score > best_score:
			best_score = score
			chosen = Task.SERVE
			chosen_dish = ready_dish
			chosen_position = ready_dish.global_position

	var dirty_dish := _dirty_dish_on_table()
	if dirty_dish != null:
		var score := 0.8 * task_preference("bus")
		if score > best_score:
			best_score = score
			chosen = Task.BUS
			chosen_dish = dirty_dish
			chosen_position = dirty_dish.global_position

	if not restaurant.takeout_server_is_assigned() and restaurant.takeout_needs_server():
		var score: float = float(restaurant.takeout_server_priority(self))
		if score > best_score:
			best_score = score
			chosen = Task.TAKEOUT_SERVICE
			chosen_table = -1
			chosen_dish = null
			chosen_position = restaurant.takeout_server_position()
	if chosen == Task.TAKEOUT_SERVICE and not restaurant.assign_takeout_server(self):
		chosen = Task.NONE
		chosen_position = Vector2.ZERO

	task = chosen
	stage = Stage.GO
	target_table = chosen_table
	target_dish = chosen_dish
	target_position = chosen_position

func _choose_cook_task() -> void:
	var station := _free_unclaimed_station()
	var line := GameState.claimable_order_line(_cookable_recipes())
	var dining_score := 1.0 if station >= 0 and not line.is_empty() else 0.0
	var takeout_score := 0.0
	if not restaurant.takeout_cook_is_assigned() and restaurant.takeout_needs_cook():
		takeout_score = restaurant.takeout_cook_priority()
	if takeout_score > dining_score and restaurant.assign_takeout_cook(self):
		task = Task.TAKEOUT
		stage = Stage.GO
		target_position = restaurant.takeout_prop.global_position
		return
	if dining_score <= 0.0:
		return
	task = Task.COOK
	stage = Stage.GO
	target_station = station
	target_table = int(line.get("table", -1))
	target_position = restaurant.stations[station].prop.global_position
	# La ligne est réservée dès le choix: deux cuisiniers ne se disputent jamais
	# la même commande, et le joueur non plus.
	GameState.reserve_order_line(target_table, station)

# Un four visé par un collègue n'est pas libre. `station_is_busy` ne devient
# vrai qu'à l'allumage: pendant la marche, deux cuisiniers choisissaient le même
# four et le second arrivait pour rien. C'est la réservation des assiettes,
# appliquée aux postes.
func _free_unclaimed_station() -> int:
	for index: int in range(restaurant.visible_station_count()):
		if restaurant.station_kind(index) != "stove" or restaurant.station_is_busy(index):
			continue
		if not _station_is_claimed(index):
			return index
	return -1

func _station_is_claimed(index: int) -> bool:
	for node: Node in get_tree().get_nodes_in_group("employee"):
		if node == self or not node is Employee:
			continue
		var other := node as Employee
		if other.task == Task.COOK and other.target_station == index:
			return true
	return false

# Sans tâche, on ne reste pas planté à côté d'un client qui mange: on regagne le
# poste d'attente, d'où la prochaine tâche est à mi-chemin. C'est du placement,
# pas une tâche — donc rien à réserver et rien à abandonner.
func _idle_step(delta: float) -> void:
	var rest: Vector2 = restaurant.employee_rest_position(self)
	if global_position.distance_to(rest) <= ARRIVAL_DISTANCE:
		return
	global_position = global_position.move_toward(rest, move_speed() * delta)
	queue_redraw()

# Recettes que ce cuisinier peut réellement produire maintenant.
func _cookable_recipes() -> Array[String]:
	var result: Array[String] = []
	for recipe_id: String in DataCatalog.RECIPES:
		if GameState.can_cook(recipe_id):
			result.append(recipe_id)
	return result

func _customer_needing_order() -> Customer:
	for guest: Customer in restaurant.customers:
		if not is_instance_valid(guest) or not guest.can_take_order():
			continue
		if guest.table_index < 0 or GameState.board_has_line(guest.table_index):
			continue
		if global_position.distance_to(guest.global_position) > sight_range():
			continue
		return guest
	return null

# Une assiette prête n'est bonne à porter que si quelqu'un l'attend.
func _dish_ready_to_serve() -> Dish:
	for node: Node in get_tree().get_nodes_in_group("dish"):
		if not node is Dish:
			continue
		var dish := node as Dish
		if dish.state != Dish.State.FILLED or dish.carrier != null or _is_claimed(dish):
			continue
		if _customer_waiting_for(dish.recipe_id) != null:
			return dish
	return null

func _dirty_dish_on_table() -> Dish:
	for node: Node in get_tree().get_nodes_in_group("dish"):
		if not node is Dish:
			continue
		var dish := node as Dish
		if dish.state == Dish.State.DIRTY and dish.carrier == null and not _is_claimed(dish):
			return dish
	return null

# Une assiette déjà visée par un collègue n'est pas reprise: sans ça deux
# serveurs convergeraient sur la même et l'un des deux repartirait bredouille.
func _is_claimed(dish: Dish) -> bool:
	for other: Node in get_tree().get_nodes_in_group("employee"):
		if other == self or not other is Employee:
			continue
		if (other as Employee).target_dish == dish:
			return true
	return false

func _customer_waiting_for(recipe_id: String) -> Customer:
	for guest: Customer in restaurant.customers:
		if is_instance_valid(guest) and guest.state == Customer.State.WAITING and guest.requested_recipe == recipe_id:
			return guest
	return null

# --- Exécution ---------------------------------------------------------------

# Ramasser est bref: c'est porter qui coûte du temps, et ce temps est déjà celui
# de la marche.
func _begin_action() -> void:
	if stage == Stage.GO and (task == Task.SERVE or task == Task.BUS):
		action_remaining = action_time("pace") * 0.35
		return
	match task:
		Task.TAKE_ORDER: action_remaining = action_time("attentiveness")
		Task.SERVE: action_remaining = action_time("pace")
		Task.BUS: action_remaining = action_time("tidiness")
		Task.COOK: action_remaining = action_time("mise")
		Task.TAKEOUT: action_remaining = action_time("mise")
		Task.TAKEOUT_SERVICE: action_remaining = action_time("pace")

func _finish_task() -> void:
	if task == Task.TAKEOUT or task == Task.TAKEOUT_SERVICE:
		stage = Stage.TEND
		target_position = global_position
		return
	# Premier temps d'une tâche de portage: on prend l'assiette et on repart.
	# La tâche n'est pas finie, elle change seulement de destination.
	if stage == Stage.GO and (task == Task.SERVE or task == Task.BUS):
		if not _take_dish():
			_abandon_task()
			return
		stage = Stage.CARRY
		target_position = _carry_destination()
		action_remaining = 0.0
		return
	# Dernier temps du cuisinier: l'assiette se pose sur l'étagère.
	if task == Task.COOK and stage == Stage.CARRY:
		_shelve_held_dish()
		_clear_task()
		return
	match task:
		Task.TAKE_ORDER: _do_take_order()
		Task.SERVE:
			_do_serve()
			# Le plat propre a été échangé contre l'assiette sale de la table: la
			# tâche continue vers la plonge au lieu de s'arrêter les mains vides.
			if task == Task.BUS and stage == Stage.CARRY:
				return
		Task.BUS: _do_bus()
		Task.COOK:
			_do_cook()
			# Le four est allumé: on reste devant jusqu'à ce que le plat sorte.
			if task == Task.COOK and restaurant.station_is_busy(target_station):
				stage = Stage.TEND
				target_position = global_position
				return
	_clear_task()

# Le service a rendu une assiette sale en échange du plat propre. Le serveur
# enchaîne vers la plonge: la vaisselle est déjà en main, il ne repart pas la
# chercher. Appelé par le restaurant pendant `serve_customer`.
func take_swapped_plate(dirty: Dish) -> void:
	held = dirty
	target_dish = dirty
	task = Task.BUS
	stage = Stage.CARRY
	action_remaining = 0.0
	target_position = _carry_destination()

# L'assiette passe en main. C'est le même geste que celui du joueur, sans le
# message: un employé ne commente pas ce qu'il fait.
func _take_dish() -> bool:
	if target_dish == null or not is_instance_valid(target_dish) or target_dish.carrier != null:
		return false
	target_dish.carry_by(self)
	held = target_dish
	return true

# Le cuisinier reste devant son four jusqu'à ce que le plat sorte, puis le porte
# lui-même. Le plat apparaît **sur le four**: si le serveur ou le joueur passe
# avant lui, tant mieux — il le constate et retourne à son poste.
func _tend_station() -> void:
	if target_station < 0 or target_station >= restaurant.stations.size():
		_clear_task()
		return
	if restaurant.station_is_busy(target_station):
		return
	var plated := _dish_at_station(target_station)
	if plated == null:
		# Quelqu'un a été plus rapide: il n'y a plus rien à porter.
		_clear_task()
		return
	plated.carry_by(self)
	held = plated
	target_dish = plated
	stage = Stage.CARRY
	target_position = restaurant.free_shelf_position()

func is_tending_takeout() -> bool:
	return task == Task.TAKEOUT and stage == Stage.TEND and restaurant != null \
		and is_instance_valid(restaurant) and restaurant.takeout_cook_is_assigned(self)

func _tend_takeout() -> void:
	if not restaurant.takeout_needs_cook():
		_clear_task()

func is_tending_takeout_service() -> bool:
	return task == Task.TAKEOUT_SERVICE and stage == Stage.TEND and restaurant != null \
		and is_instance_valid(restaurant) and restaurant.takeout_server_is_assigned(self)

func _tend_takeout_service() -> void:
	if not restaurant.takeout_needs_server():
		_clear_task()

# L'assiette qui vient de sortir de ce poste-là. Reconnue à sa place, pas à un
# drapeau: le poste ne garde aucune trace de ce qu'il a produit.
func _dish_at_station(index: int) -> Dish:
	var point: Vector2 = restaurant.get_plating_point(index)
	for node: Node in get_tree().get_nodes_in_group("dish"):
		if not node is Dish:
			continue
		var plate := node as Dish
		if plate.state != Dish.State.FILLED or plate.carrier != null:
			continue
		if plate.global_position.distance_to(point) <= 44.0:
			return plate
	return null

# La place se redemande au moment de poser: celle visée au départ a pu être
# prise pendant la marche. Une étagère pleine ne bloque pas le cuisinier, elle
# lui fait poser l'assiette au pied du meuble.
func _shelve_held_dish() -> void:
	if held == null or not is_instance_valid(held):
		return
	held.place_on(restaurant.free_shelf_position())
	held = null

func _carry_destination() -> Vector2:
	if task == Task.BUS:
		return restaurant.dishwasher_position()
	var guest := _customer_waiting_for(held.recipe_id) if held != null else null
	return guest.global_position if guest != null else global_position

func _clear_task() -> void:
	var had_task := task != Task.NONE
	if task == Task.TAKEOUT and restaurant != null and is_instance_valid(restaurant):
		restaurant.release_takeout_cook(self)
	if task == Task.TAKEOUT_SERVICE and restaurant != null and is_instance_valid(restaurant):
		restaurant.release_takeout_server(self)
	task = Task.NONE
	stage = Stage.GO
	target_table = -1
	target_station = -1
	target_dish = null
	held = null
	action_remaining = 0.0
	if had_task:
		say("task_ended")

# Le serveur écrit sur l'ardoise exactement comme le joueur: même appel, même
# règle. Une ardoise pleine le laisse repartir sans rien casser.
func _do_take_order() -> void:
	for guest: Customer in restaurant.customers:
		if is_instance_valid(guest) and guest.table_index == target_table and guest.can_take_order():
			GameState.write_order_line(target_table, guest.requested_recipe)
			# La prise se note au charisme de ce serveur-ci: c'est ce qui
			# distingue deux employés sur la même attente.
			guest.take_order_by(maxf(0.0, skill("charisma")))
			GameState.add_entity_counter("employees", employee_id, "total_meals_served", 0)
			return

func _do_serve() -> void:
	var target_dish := held
	if target_dish == null or not is_instance_valid(target_dish):
		return
	var guest := _customer_waiting_for(target_dish.recipe_id)
	if guest == null:
		return
	# Le charisme se paie sur ce qu'il sert lui-même, mais il ne se bricole plus
	# ici: `serve_customer` lit le porteur de l'assiette et note l'attente à son
	# nom. L'ancien calcul multipliait un pourboire encore nul, puis se faisait
	# écraser par le dépôt — il n'a jamais rien rapporté à personne.
	restaurant.serve_customer(guest, target_dish)
	GameState.add_entity_counter("employees", employee_id, "total_meals_served", 1)

func _do_bus() -> void:
	var target_dish := held
	if target_dish == null or not is_instance_valid(target_dish):
		return
	# Table libérée et paiement encaissé au ramassage, comme pour le joueur.
	# L'appel reste un filet: une assiette restaurée depuis la sauvegarde arrive
	# en main sans être passée par le ramassage, et `payment_claimed` le rend
	# inoffensif si elle l'a déjà été.
	restaurant.claim_dirty_plate_payment(target_dish)
	target_dish.wash()
	GameState.add_entity_counter("employees", employee_id, "total_plates_cleared", 1)

func _do_cook() -> void:
	if target_station < 0 or restaurant.station_is_busy(target_station):
		GameState.release_order_line(target_table)
		return
	var line := GameState.order_board_line(target_table)
	if line.is_empty():
		return
	restaurant.start_cooking(str(line.get("recipe", "")), target_station)
	if restaurant.station_is_busy(target_station):
		restaurant.assign_cook(target_station, self)
		restaurant.stations[target_station].order_table = target_table
		GameState.add_entity_counter("employees", employee_id, "total_dishes_cooked", 1)
	else:
		GameState.release_order_line(target_table)

func _draw() -> void:
	# La branche 3D donne un corps à cette entité: son dessin 2D se tait.
	# Une ligne, et aucune règle ne bouge — la 2D reste l'autorité.
	if Render3D.active:
		return
	draw_circle(Vector2.ZERO, BODY_RADIUS, body_color)
	draw_circle(Vector2.ZERO, BODY_RADIUS, body_color.lightened(0.4), false, 3.0)
	# Un liseré dit le métier sans texte: carré pour la cuisine, rond pour la salle.
	if role == "cook":
		draw_rect(Rect2(-6, -22, 12, 6), Color("20152c"))
	if task != Task.NONE:
		draw_circle(Vector2(0, -24), 3.5, Color("ffd166"))
	if is_tending_takeout_service():
		draw_string(ThemeDB.fallback_font, Vector2(-30, -31), "+30%", HORIZONTAL_ALIGNMENT_CENTER, 60, 12, Color("4ade80"))

func _exit_tree() -> void:
	# Une assiette accrochée à un employé mis en stand-by ou retiré par sa portée
	# serait perdue pour tout le monde: elle se repose là où il se trouvait.
	if held != null and is_instance_valid(held):
		held.place_on(global_position)
		held = null
	if target_table >= 0 and task == Task.COOK and stage == Stage.GO:
		GameState.release_order_line(target_table)
	if task == Task.TAKEOUT and restaurant != null and is_instance_valid(restaurant):
		restaurant.release_takeout_cook(self)
	if task == Task.TAKEOUT_SERVICE and restaurant != null and is_instance_valid(restaurant):
		restaurant.release_takeout_server(self)
