class_name SleepProvider extends WorkProvider
## WorkProvider for the Sleep work category. Slots into the 5-layer pawn AI
## (Decision → WorkProvider → Job + JobRunner) at priority 8 — above eating (7)
## so a maximally tired AND hungry pawn prioritises rest.
##
## When a pawn is tired and not carrying an item, scans World.beds for the
## nearest available bed and builds a 2-toil sleep job: walk → sleep.
## If no bed is available, the pawn sleeps on the floor at its current tile
## (walk toil omitted since it's already there).
##
## Bed claim race: the bed is NOT claimed here. Claiming in find_best_for()
## would leak a held claim if the job is later cancelled by a player override
## (Layer 5) before execution starts. Instead _tick_sleep in JobRunner claims
## on first tick, when the pawn has actually arrived and is ready to sleep.
## If the claim fails at that point (another pawn beat us) the fallback is
## floor sleep — no deadlock, no leaked claim.
##
## Pawn and Bed are intentionally duck-typed to avoid the class_name
## registration-order trap documented in Phase 2/3.
##
## See docs/architecture.md "Pawn AI 5-layer pipeline" and
##     docs/design.md "Sleep mood" gradient.


func _init() -> void:
	category = &"sleep"
	# Priority 8 > eat (7) > construction (6) > hauling (3) > rest (0).
	# Phase 9 status interrupts will preempt from Decision layer 3 once the
	# Status registry lands — for now sleep rides at the WorkProvider level.
	priority = 8


# ── WorkProvider override ─────────────────────────────────────────────────────

## Returns a sleep Job for `pawn`, or null if the pawn is not tired.
## The job has 1 or 2 toils:
##   - walk_to(target_tile)  [omitted when target == pawn.tile]
##   - sleep_in_bed(bed_path) [bed_path is empty string for floor sleep]
##
## `pawn` is duck-typed: must expose .is_tired(), .carried_item, .tile (Vector2i).
func find_best_for(pawn) -> Job:
	if not pawn.is_tired():
		return null
	# Don't interrupt an ongoing carry — finish the carry before going to sleep.
	if pawn.carried_item != null:
		return null

	# Scan World.beds for the nearest available bed (duck-typed — Bed exposes
	# .is_available() -> bool and .tile -> Vector2i).
	var best_bed = null
	var best_dist: int = 999999
	for bed in World.beds:
		if not bed.is_available():
			continue
		# Skip beds already targeted by another pawn's active job.  This is the
		# same deconfliction used by ConstructionProvider (Job.is_target_taken_by_other)
		# and prevents all simultaneously-tired pawns from converging on the same
		# nearest bed and then losing the race at claim time.
		if Job.is_target_taken_by_other(bed, pawn):
			continue
		var d: int = abs(bed.tile.x - pawn.tile.x) + abs(bed.tile.y - pawn.tile.y)
		if d < best_dist:
			best_dist = d
			best_bed = bed

	# Resolve target tile and bed NodePath.  An empty NodePath means floor sleep.
	var target_tile: Vector2i
	var bed_path: NodePath
	if best_bed != null:
		target_tile = best_bed.tile
		bed_path = best_bed.get_path()
	else:
		# Phase 8 fallback — sleep on the floor at the current tile.
		# The Mood system (Agent C, Phase 8) will fire a "slept_on_floor" thought
		# via the thought registry; analogous to EatProvider's "ate raw food".
		target_tile = pawn.tile
		bed_path = NodePath("")

	var j := Job.new()
	if best_bed != null:
		j.label = "Sleep at %s" % target_tile
		# Tag the job's target so other pawns see this bed as taken via
		# Job.is_target_taken_by_other() before it is physically claimed.
		j.target_node = best_bed
	else:
		j.label = "Sleep on the floor at %s" % target_tile

	# Only prepend a walk if the pawn is not already at the target.
	if pawn.tile != target_tile:
		j.toils.append(Toil.walk_to(target_tile))

	j.toils.append(Toil.sleep_in_bed(bed_path))
	return j
