class_name Decision
## Static utility — picks the next Job for a pawn via the 5-layer pipeline.
##
## Layer order (top wins):
##   1. Incapacitation  — has_method probe; implementation lands Phase 9.
##   2. Forced job      — pawn.forced_job; cleared (consumed) on pick.
##   3. Status interrupt — stub; implementation lands Phase 9.
##   4. Work providers  — iterated highest priority first; first non-null Job wins.
##   5. Idle            — returns null (caller interprets as "stand still").
##
## Callers pass the world-scoped provider list so Decision is fully stateless.
## This makes it safe to call from any pawn tick without shared mutable state.


## Returns the best Job for `pawn`, or null if the pawn should idle.
##
## `work_providers` is the current world-scoped list of WorkProvider nodes
## (e.g. [RestProvider]). Order does not matter — the method sorts by priority.
## `pawn` is duck-typed: must expose .pawn_name, .forced_job, and
## has_method("is_incapacitated").
static func pick_next_job(pawn, work_providers: Array) -> Job:
	# ── Layer 1: Incapacitation / sulking ───────────────────────────────────
	# has_method probes so these don't break before Phase 9 adds is_incapacitated.
	if pawn.has_method("is_incapacitated") and pawn.is_incapacitated():
		return null
	# Sulking pawns refuse all jobs until mood recovers to MOOD_SULK_RECOVERY.
	# Phase 17 may replace this with a Wandering soft-break variant that moves
	# the pawn to a quiet tile instead of standing still.
	if pawn.has_method("is_sulking") and pawn.is_sulking():
		return null

	# ── Layer 2: Forced job ──────────────────────────────────────────────────
	if pawn.forced_job != null:
		var fj: Job = pawn.forced_job
		pawn.forced_job = null
		Audit.log("decision", "%s: forced '%s'" % [pawn.pawn_name, fj.label])
		return fj

	# ── Layer 3: Status interrupt ─────────────────────────────────────────────
	# Phase 9: status interrupt (Bleeding → seek bed/doctor) lands here.

	# ── Layer 4: Work providers ──────────────────────────────────────────────
	# Needs-driven categories are handled entirely by Layer-3 status interrupts
	# and need-threshold providers (eat/sleep/doctor fire when hunger/sleep
	# thresholds trigger, not via player priority). We skip the priority filter
	# for them so a pawn can never accidentally starve because the player set
	# &"eat" to OFF. The player-configurable list is the 7 work categories:
	#   construction / chop / plant / mine / crafting / haul / clean
	# Doctor IS in the matrix (player can opt a pawn out of doctor duty) but
	# the needs-driven "go heal yourself" path bypasses this filter at Layer 3.
	#
	# Rest is NOT a need-gated category — RestProvider always returns a job (its
	# job IS "stand here"), so if rest ran before elective work, pawns would
	# never chop/mine/build. Rest is the eligible-bucket fallback: it sorts last
	# because provider.priority=0, so any real work wins. Bug ref: 2026-05-12
	# session — pawns idled with valid chop/mine/build designations because
	# &"rest" was in NEEDS_CATEGORIES and RestProvider preempted everything.
	const NEEDS_CATEGORIES: Array = [&"eat", &"sleep"]

	# Pawn's work_priorities dict — empty dict if the field is absent (pre-Phase-17
	# pawns loaded from old saves). Missing category key defaults to 3 (NORMAL) so
	# legacy behaviour is preserved exactly.
	var priorities: Dictionary = {}
	if pawn.get("work_priorities") != null:
		priorities = pawn.work_priorities

	# Partition providers: needs bypass the matrix; work providers are filtered.
	var eligible: Array = []
	var needs: Array = []
	for wp in work_providers:
		if wp.category in NEEDS_CATEGORIES:
			needs.append(wp)
		else:
			# OFF (0) means pawn refuses this category entirely.
			var lvl: int = int(priorities.get(wp.category, 3))
			if lvl > 0:
				eligible.append(wp)

	# Sort eligible by pawn priority ascending (Critical=1 first, Low=4 last),
	# then by provider.priority descending as tiebreaker.
	eligible.sort_custom(func(a, b) -> bool:
		var pa: int = int(priorities.get(a.category, 3))
		var pb: int = int(priorities.get(b.category, 3))
		if pa != pb:
			return pa < pb   # lower pawn-priority-number = higher precedence
		return a.priority > b.priority  # higher provider.priority = first within tier
	)

	# Needs providers retain their natural order (sorted by provider.priority only).
	needs.sort_custom(func(a, b): return a.priority > b.priority)

	# Try needs first (hunger/sleep/rest fire before elective work), then eligible.
	var to_try: Array = needs + eligible

	for wp in to_try:
		var j: Job = wp.find_best_for(pawn)
		if j != null:
			var lvl_str: String = ""
			if not (wp.category in NEEDS_CATEGORIES):
				lvl_str = "(pri=%d)" % int(priorities.get(wp.category, 3))
			Audit.log("decision", "%s: %s%s → '%s'" % [pawn.pawn_name, String(wp.category), lvl_str, j.label])
			return j

	# ── Layer 5: Idle ────────────────────────────────────────────────────────
	# No log — would fire every tick for every idle pawn (too chatty).
	return null
