extends Object

## Pure resolver for digit hotbar → cast intent (NEO-31). [code]main.gd[/code] delegates here so
## GdUnit can cover unbound slots and [code]lockedTargetId[/code] sourcing without booting
## the scene.
## No [code]class_name[/code] — match [code]prototype_target_constants.gd[/code] preload pattern.

const KEY_OK := "ok"
const KEY_REASON := "reason"
const KEY_SLOT_INDEX := "slotIndex"
const KEY_ABILITY_ID := "abilityId"
const KEY_TARGET_ID := "targetId"

const REASON_INVALID_SLOT := "invalid_slot_index"
const REASON_EMPTY_SLOT := "empty_slot"


## [param slots] snapshot from [code]HotbarState.slots_snapshot()[/code] (fixed length, entries
## [code]null[/code] or [String] ability ids).
## [param target_cached_state] from [code]TargetSelectionClient.cached_state()[/code]
## (may be empty).
## Returns [code]{ "ok": true, "slotIndex", "abilityId", "targetId" }[/code] or
## [code]{ "ok": false, "reason" }[/code].
static func resolve(slot_index: int, slots: Array, target_cached_state: Dictionary) -> Dictionary:
	if slot_index < 0 or slot_index >= slots.size():
		return {KEY_OK: false, KEY_REASON: REASON_INVALID_SLOT}
	var ability_variant: Variant = slots[slot_index]
	if (
		ability_variant == null
		or not (ability_variant is String)
		or (ability_variant as String).strip_edges().is_empty()
	):
		return {KEY_OK: false, KEY_REASON: REASON_EMPTY_SLOT}
	var ability_id: String = (ability_variant as String).strip_edges()
	var target_id: Variant = null
	var locked: Variant = target_cached_state.get("lockedTargetId", null)
	if locked is String and not (locked as String).is_empty():
		target_id = locked as String
	return {
		KEY_OK: true,
		KEY_SLOT_INDEX: slot_index,
		KEY_ABILITY_ID: ability_id,
		KEY_TARGET_ID: target_id,
	}
