extends Node
## SaveSystem v2 — full entity serialization + clear-and-respawn load.
##
## Save contract (docs/architecture.md "Save format"):
##   - Saves only happen between sim ticks (Sim owns the loop). apply_save()
##     pauses the sim so no tick fires mid-rebuild.
##   - Every entity's to_dict() must include a "class_id" field (Agent A's work).
##     The factory registry here dispatches on that key.
##   - Save files: user://save_<slot>.json  (slot = StringName, e.g. &"manual").
##   - Autosave uses slot &"autosave"; manual save uses &"manual".
##
## Public slot API (Agent C surface):
##   save_to_slot(slot)          → bool
##   load_from_slot(slot)        → bool
##   has_save(slot)              → bool
##   delete_save(slot)           → bool
##   peek_save_metadata(slot)    → {exists, saved_at_unix, version}
##
## Phase 16 — replaces the Phase 3 pawn-only stub.

const SAVE_VERSION: int = 2


# ── PackedScene preloads (mirror world.gd constants) ──────────────────────────
# Keep in sync with scenes/world/world.gd SCENE constants.
# If a scene path changes, update both files.

const _PAWN_SCENE:          PackedScene = preload("res://scenes/pawn/pawn.tscn")
const _TREE_SCENE:          PackedScene = preload("res://scenes/entities/tree.tscn")
const _ROCK_SCENE:          PackedScene = preload("res://scenes/entities/rock.tscn")
const _BIG_ROCK_SCENE:      PackedScene = preload("res://scenes/entities/big_rock.tscn")
const _BIG_ROCK_NODE_SCENE: PackedScene = preload("res://scenes/entities/big_rock_node.tscn")
const _ITEM_SCENE:          PackedScene = preload("res://scenes/entities/item.tscn")
const _WALL_SCENE:          PackedScene = preload("res://scenes/entities/wall.tscn")
const _FLOOR_SCENE:         PackedScene = preload("res://scenes/entities/floor.tscn")
const _DOOR_SCENE:          PackedScene = preload("res://scenes/entities/door.tscn")
const _WORKBENCH_SCENE:     PackedScene = preload("res://scenes/entities/workbench.tscn")
const _CROP_SCENE:          PackedScene = preload("res://scenes/entities/crop.tscn")
const _BED_SCENE:           PackedScene = preload("res://scenes/entities/bed.tscn")
const _TORCH_SCENE:         PackedScene = preload("res://scenes/entities/torch.tscn")
const _STOCKPILE_SCENE:     PackedScene = preload("res://scenes/world/stockpile_zone.tscn")
const _CRATE_SCENE:         PackedScene = preload("res://scenes/world/crate.tscn")
const _WOLF_SCENE:          PackedScene = preload("res://scenes/entities/wolf.tscn")
const _CROP_SCRIPT:         Script      = preload("res://scenes/entities/crop.gd")
const _CORPSE_SCENE:        PackedScene = preload("res://scenes/entities/corpse.tscn")

# Script-only entities (no .tscn); spawned via Node2D.new() + set_script().
const _GRAVEYARD_ZONE_SCRIPT: Script = preload("res://scenes/world/graveyard_zone.gd")
const _GRAVE_SLOT_SCRIPT:     Script = preload("res://scenes/entities/grave_slot.gd")
const _GRAVE_MARKER_SCRIPT:   Script = preload("res://scenes/entities/grave_marker.gd")


# ── factory registry ───────────────────────────────────────────────────────────
# Keys are the class_id strings that entities embed in their to_dict() output.
# Values are Callables: func(world_scene: Node, d: Dictionary) -> void.
# Populated in _ready(); dispatch via _spawn_entity().

var _factories: Dictionary = {}


func _ready() -> void:
	_register_factories()


# ── public slot API ────────────────────────────────────────────────────────────

## Write the current world state to the named slot. Returns true on success.
func save_to_slot(slot: StringName) -> bool:
	return write_save(slot)


## Load the named slot and apply it to the live world. Returns true on success.
func load_from_slot(slot: StringName) -> bool:
	var payload := read_save(slot)
	if payload.is_empty():
		EventBus.load_finished.emit(slot, false, 0)
		return false
	apply_save(payload, slot)
	return true


## True if a save file for this slot exists on disk.
func has_save(slot: StringName) -> bool:
	return FileAccess.file_exists(_path_for(slot))


## Delete the save file for the given slot. Returns true if the file was removed.
func delete_save(slot: StringName) -> bool:
	var path := _path_for(slot)
	if not FileAccess.file_exists(path):
		return false
	var err := DirAccess.remove_absolute(path)
	if err != OK:
		push_error("SaveSystem.delete_save: cannot delete '%s' (err %d)" % [path, err])
		return false
	Audit.log("save", "deleted slot '%s'" % slot)
	return true


## Peek at a slot's metadata without a full load. Returns a Dictionary:
##   {exists: bool, saved_at_unix: int, version: int}
func peek_save_metadata(slot: StringName) -> Dictionary:
	var path := _path_for(slot)
	if not FileAccess.file_exists(path):
		return {exists = false, saved_at_unix = 0, version = 0}
	var f := FileAccess.open(path, FileAccess.READ)
	if f == null:
		return {exists = false, saved_at_unix = 0, version = 0}
	var parsed: Variant = JSON.parse_string(f.get_as_text())
	if typeof(parsed) != TYPE_DICTIONARY:
		return {exists = true, saved_at_unix = 0, version = 0}
	return {
		exists        = true,
		saved_at_unix = int(parsed.get("saved_at_unix", 0)),
		version       = int(parsed.get("version", 0)),
	}


# ── core write / read / apply ──────────────────────────────────────────────────

func write_save(slot: StringName = &"manual") -> bool:
	# Pause the sim so we snapshot between ticks (never mid-tick).
	var prev_speed: Sim.Speed = Sim.current_speed
	Sim.set_speed(Sim.Speed.PAUSE)

	EventBus.save_started.emit(slot)

	var entities := _collect_entities()

	var payload: Dictionary = {
		"version":        SAVE_VERSION,
		"saved_at_unix":  int(Time.get_unix_time_from_system()),
		"saved_at_tick":  Sim.tick,
		"sim_speed":      int(prev_speed),
		"game_state":     GameState.save_dict(),
		"clock":          Clock.save_dict(),
		"weather":        Weather.save_dict(),
		"storyteller":    Storyteller.save_dict(),
		"tilemap_layers": _save_tilemap_layers_safe(),
		"beauty_map":     _save_beauty_safe(),
		"dirt_map":       _save_dirt_safe(),
		"entities":       entities,
	}

	var ok := _write_json(slot, payload)

	if ok:
		Audit.log("save", "wrote slot '%s': %d entities at tick %d" % [
			slot, entities.size(), Sim.tick
		])
	else:
		push_error("SaveSystem.write_save: file write failed for slot '%s'" % slot)

	EventBus.save_finished.emit(slot, ok)
	Sim.set_speed(prev_speed)
	return ok


func read_save(slot: StringName = &"manual") -> Dictionary:
	var path := _path_for(slot)
	if not FileAccess.file_exists(path):
		return {}
	var f := FileAccess.open(path, FileAccess.READ)
	if f == null:
		push_error("SaveSystem.read_save: cannot open '%s'" % path)
		return {}
	var parsed: Variant = JSON.parse_string(f.get_as_text())
	if typeof(parsed) != TYPE_DICTIONARY:
		push_error("SaveSystem.read_save: corrupt save at '%s'" % path)
		return {}
	var ver: int = int(parsed.get("version", 0))
	if ver != SAVE_VERSION:
		push_warning(
			"SaveSystem.read_save: version mismatch in '%s' (file v%d vs code v%d) — best-effort load" %
			[path, ver, SAVE_VERSION]
		)
	return parsed


## Apply a loaded payload onto the live world. slot is used only for signal emission.
## On any per-entity error: logs via push_error, skips that entity, keeps going.
## On catastrophic failure (no world scene): emits load_finished with ok=false; never crashes.
func apply_save(payload: Dictionary, slot: StringName = &"manual") -> void:
	EventBus.load_started.emit(slot)

	var prev_speed: Sim.Speed = Sim.current_speed
	Sim.set_speed(Sim.Speed.PAUSE)

	var real_seconds_away: int = max(
		0,
		int(Time.get_unix_time_from_system()) - int(payload.get("saved_at_unix", 0))
	)

	# Clear every entity registry + queue_free their nodes.
	World.clear_all()

	# Restore sim tick counter.
	Sim.tick = int(payload.get("saved_at_tick", 0))

	# Restore autoload state in dependency order.
	if payload.has("game_state"):
		GameState.apply_dict(payload["game_state"])
	if payload.has("clock"):
		Clock.apply_dict(payload["clock"])
	if payload.has("weather"):
		Weather.apply_dict(payload["weather"])
	if payload.has("storyteller"):
		Storyteller.apply_dict(payload["storyteller"])

	# Restore TileMap layers (Agent A adds World.save/apply_tilemap_layers).
	var tilemap_data: Dictionary = payload.get("tilemap_layers", {})
	if not tilemap_data.is_empty():
		_apply_tilemap_layers_safe(tilemap_data)

	# Locate the world scene node (parent of all entity children).
	var world_scene := _get_world_scene()
	if world_scene == null:
		push_error("SaveSystem.apply_save: cannot locate World scene node — aborting entity spawn")
		EventBus.load_finished.emit(slot, false, real_seconds_away)
		Sim.set_speed(prev_speed)
		return

	# Respawn entities, sorted so dependencies land before dependents.
	var entity_dicts: Array = payload.get("entities", [])
	var ordered := _sort_by_spawn_priority(entity_dicts)
	var entity_count := 0
	var error_count := 0

	for d in ordered:
		if typeof(d) != TYPE_DICTIONARY:
			continue
		var cid: StringName = StringName(d.get("class_id", ""))
		if cid == &"":
			push_error("SaveSystem.apply_save: entity dict missing class_id — skipped: %s" % d)
			error_count += 1
			continue
		if not _factories.has(cid):
			push_error("SaveSystem.apply_save: no factory for class_id '%s' — skipped" % cid)
			error_count += 1
			continue
		var factory: Callable = _factories[cid]
		factory.call(world_scene, d)
		entity_count += 1

	# Restore beauty / dirt maps after entities are back.
	var beauty_data: Dictionary = payload.get("beauty_map", {})
	if not beauty_data.is_empty() and World.beauty_system != null:
		if World.beauty_system.has_method("apply_dict"):
			World.beauty_system.apply_dict(beauty_data)

	var dirt_data: Dictionary = payload.get("dirt_map", {})
	if not dirt_data.is_empty() and World.dirtiness_system != null:
		if World.dirtiness_system.has_method("apply_dict"):
			World.dirtiness_system.apply_dict(dirt_data)

	# Restore sim speed to what it was when the save was taken.
	var saved_speed_int: int = int(payload.get("sim_speed", int(Sim.Speed.NORMAL)))
	var saved_speed: Sim.Speed = Sim.Speed.NORMAL
	if saved_speed_int >= 0 and saved_speed_int < Sim.Speed.size():
		saved_speed = saved_speed_int as Sim.Speed

	# Resolve cross-entity references that require all nodes to be spawned first.
	_post_load_resolve_references()

	var ok: bool = error_count == 0
	Audit.log("save", "applied slot '%s': %d entities, %d errors, tick=%d, away=%ds" % [
		slot, entity_count, error_count, Sim.tick, real_seconds_away
	])

	EventBus.load_finished.emit(slot, ok, real_seconds_away)
	Sim.set_speed(saved_speed)


# ── entity collection ──────────────────────────────────────────────────────────

## Walk every entity registry on World; call to_dict() on each valid node.
## Concatenated into a single flat Array[Dictionary] — class_id is the dispatcher key.
func _collect_entities() -> Array:
	var out: Array = []
	# Registries in stable order. Pawn last so dependents (workbenches, beds) precede.
	var registries: Array = [
		World.trees,
		World.rocks,
		World.big_rock_nodes,
		World.items,
		World.build_queue,    # ghost walls / floors / doors / grave_slots
		World.doors,
		World.workbenches,
		World.crops,
		World.beds,
		World.light_sources,  # torches (hearth is a workbench — also in workbenches)
		World.stockpiles,
		World.wolves,
		World.corpses,
		World.grave_markers,
		World.pawns,
	]
	for reg in registries:
		for ent in reg:
			if not is_instance_valid(ent):
				continue
			if ent.has_method("to_dict"):
				var d: Dictionary = ent.to_dict()
				if d.has("class_id"):
					out.append(d)
				else:
					push_warning("SaveSystem._collect_entities: %s.to_dict() missing class_id — skipped" % ent)
	return out


# ── spawn-order sorting ────────────────────────────────────────────────────────

## Spawn priority tiers (lower = spawns first so dependencies land before dependents).
##   0 — static structures: tree, rock, wall, floor
##   1 — doors (erase wall stamp in _complete)
##   2 — containers + furniture: crate, workbench, stockpile_zone, bed, torch, crop
##   3 — burial entities: grave_slot, grave_marker, graveyard_zone
##   4 — items + mobile non-pawn entities: item, wolf, corpse
##   5 — pawns (may reference job targets from earlier tiers)

const _SPAWN_PRIORITY: Dictionary = {
	&"tree":           0,
	&"rock":           0,
	&"big_rock":       0,
	&"big_rock_node":  0,
	&"wall":           0,
	&"floor":          0,
	&"door":           1,
	&"crate":          2,
	&"workbench":      2,
	&"stockpile_zone": 2,
	&"bed":            2,
	&"torch":          2,
	&"crop":           2,
	&"grave_slot":     3,
	&"grave_marker":   3,
	&"graveyard_zone": 3,
	&"item":           4,
	&"wolf":           4,
	&"corpse":         4,
	&"pawn":           5,
}


func _sort_by_spawn_priority(entity_dicts: Array) -> Array:
	var copy := entity_dicts.duplicate()
	copy.sort_custom(func(a, b) -> bool:
		var pa: int = _SPAWN_PRIORITY.get(StringName(a.get("class_id", "")), 99)
		var pb: int = _SPAWN_PRIORITY.get(StringName(b.get("class_id", "")), 99)
		return pa < pb
	)
	return copy


# ── factory registry setup ─────────────────────────────────────────────────────

func _register_factories() -> void:
	_factories[&"tree"]           = _spawn_tree
	_factories[&"rock"]           = _spawn_rock
	_factories[&"big_rock"]       = _spawn_big_rock
	_factories[&"big_rock_node"]  = _spawn_big_rock_node
	_factories[&"item"]           = _spawn_item
	_factories[&"wall"]           = _spawn_wall
	_factories[&"floor"]          = _spawn_floor
	_factories[&"door"]           = _spawn_door
	_factories[&"workbench"]      = _spawn_workbench
	_factories[&"crop"]           = _spawn_crop
	_factories[&"bed"]            = _spawn_bed
	_factories[&"torch"]          = _spawn_torch
	_factories[&"stockpile_zone"] = _spawn_stockpile_zone
	_factories[&"crate"]          = _spawn_crate
	_factories[&"wolf"]           = _spawn_wolf
	_factories[&"corpse"]         = _spawn_corpse
	_factories[&"grave_marker"]   = _spawn_grave_marker
	_factories[&"grave_slot"]     = _spawn_grave_slot
	_factories[&"graveyard_zone"] = _spawn_graveyard_zone
	_factories[&"pawn"]           = _spawn_pawn


# ── per-class factory functions ────────────────────────────────────────────────
# Signature: func(world_scene: Node, d: Dictionary) -> void.
# Pattern: instantiate (or new()+set_script), add_child (triggers _ready +
# self-registration), apply entity-specific from_dict or field restore.

func _spawn_tree(world_scene: Node, d: Dictionary) -> void:
	var ent = _TREE_SCENE.instantiate()
	world_scene.add_child(ent)
	# Pass growth_stage to setup() so _refresh_sprite() picks the right visual.
	# Default 3 (STAGE_MATURE) so pre-growth-system saves load as mature trees.
	var gs: int = int(d.get("growth_stage", 3))
	ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))), gs)
	ent.chop_progress = int(d.get("chop_progress", 0))
	ent.growth_progress = int(d.get("growth_progress", 0))
	ent.chop_designated = bool(d.get("chop_designated", false))
	ent.pending_plant = bool(d.get("pending_plant", false))
	ent._plant_progress = int(d.get("plant_progress", 0))
	# Re-register as build site if planting is still in progress.
	if ent.pending_plant:
		World.register_build_site(ent)
	ent.queue_redraw()


func _spawn_rock(world_scene: Node, d: Dictionary) -> void:
	var ent = _ROCK_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
	ent.mine_progress = int(d.get("mine_progress", 0))
	ent.queue_redraw()


func _spawn_big_rock(world_scene: Node, d: Dictionary) -> void:
	var ent = _BIG_ROCK_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(Vector2i(int(d.get("origin_x", 0)), int(d.get("origin_y", 0))))
	ent.mine_progress = int(d.get("mine_progress", 0))
	ent.queue_redraw()


func _spawn_big_rock_node(world_scene: Node, d: Dictionary) -> void:
	var ent = _BIG_ROCK_NODE_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
	ent.is_quarry_site = bool(d.get("is_quarry_site", false))
	ent.queue_redraw()


func _spawn_item(world_scene: Node, d: Dictionary) -> void:
	var ent = _ITEM_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(
		StringName(d.get("type", "wood")),
		int(d.get("stack_size", 1)),
		Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))
	)
	ent.quality = int(d.get("quality", 1)) as Item.Quality
	ent.subtype = StringName(d.get("subtype", ""))
	# Hauling-retry state — defaults keep older v2 saves loading cleanly.
	ent.haul_retry_count = int(d.get("haul_retry_count", 0))
	ent.haul_rejected = bool(d.get("haul_rejected", false))
	# haul_rejected items must NOT be in items_needing_haul — undo the
	# automatic enqueue that World.register_item() does inside setup().
	if ent.haul_rejected:
		World.items_needing_haul.erase(ent)
	ent.queue_redraw()


func _spawn_wall(world_scene: Node, d: Dictionary) -> void:
	var ent = _WALL_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(
		Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))),
		StringName(d.get("material", "stone"))
	)
	ent.build_progress = int(d.get("build_progress", 0))
	if bool(d.get("completed", false)):
		# Mirror _spawn_complete_wall in world.gd: set progress to BUILD_TICKS
		# then call on_build_tick() once to trigger _complete().
		ent.build_progress = ent.BUILD_TICKS
		ent.on_build_tick()
	else:
		ent.queue_redraw()


func _spawn_floor(world_scene: Node, d: Dictionary) -> void:
	var ent = _FLOOR_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(
		Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))),
		StringName(d.get("material", "wood"))
	)
	ent.build_progress = int(d.get("build_progress", 0))
	if bool(d.get("completed", false)):
		ent.build_progress = ent.BUILD_TICKS
		ent.on_build_tick()
	else:
		ent.queue_redraw()


func _spawn_door(world_scene: Node, d: Dictionary) -> void:
	var ent = _DOOR_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
	ent.build_progress = int(d.get("build_progress", 0))
	if bool(d.get("completed", false)):
		ent.build_progress = ent.BUILD_TICKS
		ent.on_build_tick()
	# Restore open state after possible completion (Door._complete sets is_open = true).
	ent.is_open = bool(d.get("is_open", true))
	if not bool(d.get("completed", false)):
		ent.queue_redraw()


func _spawn_workbench(world_scene: Node, d: Dictionary) -> void:
	var ent = _WORKBENCH_SCENE.instantiate()
	world_scene.add_child(ent)
	# from_dict restores all fields (label_text, accepted_skill, build_progress,
	# bills, deposited_inputs) and calls _complete() + setup() internally.
	# No manual field assignment needed here.
	ent.from_dict(d)


func _spawn_crop(world_scene: Node, d: Dictionary) -> void:
	var ent = _CROP_SCENE.instantiate()
	world_scene.add_child(ent)
	# Crop.from_dict() is static and returns a spec Dictionary — it cannot mutate
	# the instance. Use the spec to call setup() so tile/kind/stage are applied.
	var spec: Dictionary = _CROP_SCRIPT.from_dict(d) if d else {}
	ent.setup(
		Vector2i(int(spec.get("tile_x", 0)), int(spec.get("tile_y", 0))),
		StringName(spec.get("crop_kind", &"wheat")),
		int(spec.get("stage", 0))
	)
	ent.stage_progress = int(spec.get("stage_progress", 0))
	ent._stage_accum = float(spec.get("stage_accum", 0.0))


func _spawn_bed(world_scene: Node, d: Dictionary) -> void:
	var ent = _BED_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
	ent.build_progress = int(d.get("build_progress", 0))
	if bool(d.get("completed", false)):
		ent.build_progress = ent.BUILD_TICKS
		ent.on_build_tick()
	if ent.has_method("from_dict"):
		ent.from_dict(d)


func _spawn_torch(world_scene: Node, d: Dictionary) -> void:
	var ent = _TORCH_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
	ent.build_progress = int(d.get("build_progress", 0))
	if bool(d.get("completed", false)):
		ent.build_progress = ent.BUILD_TICKS
		ent.on_build_tick()
	if ent.has_method("from_dict"):
		ent.from_dict(d)


func _spawn_stockpile_zone(world_scene: Node, d: Dictionary) -> void:
	var ent = _STOCKPILE_SCENE.instantiate()
	world_scene.add_child(ent)
	if ent.has_method("from_dict"):
		ent.from_dict(d)
	else:
		# Fallback: wire minimal fields directly when from_dict not yet present.
		ent.region = Rect2i(
			int(d.get("region_x", 0)), int(d.get("region_y", 0)),
			int(d.get("region_w", 4)), int(d.get("region_h", 4))
		)
		ent.label = str(d.get("label", "Stockpile"))
		var pri_int: int = int(d.get("priority", 2))
		if pri_int >= 0 and pri_int < StorageDestination.Priority.size():
			ent.priority = pri_int as StorageDestination.Priority
		var types_raw: Array = d.get("accepted_types", [])
		var types: Array[StringName] = []
		for t in types_raw:
			types.append(StringName(t))
		ent.accepted_types = types
		ent.queue_redraw()


func _spawn_crate(world_scene: Node, d: Dictionary) -> void:
	var ent = _CRATE_SCENE.instantiate()
	world_scene.add_child(ent)
	ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
	ent.build_progress = int(d.get("build_progress", 0))
	if bool(d.get("completed", false)):
		# Crate uses a loop (mirrors world.gd's pre-build pattern).
		while ent.is_buildable():
			ent.on_build_tick()
	if ent.has_method("from_dict"):
		ent.from_dict(d)


func _spawn_wolf(world_scene: Node, d: Dictionary) -> void:
	var ent = _WOLF_SCENE.instantiate()
	world_scene.add_child(ent)
	if ent.has_method("from_dict"):
		ent.from_dict(d)
	else:
		# Fallback for pre-from_dict Wolf.
		ent.tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))
		ent.hp   = float(d.get("hp", 40.0))
		ent.position = Vector2(ent.tile.x * 16 + 8.0, ent.tile.y * 16 + 8.0)
		ent.queue_redraw()


func _spawn_corpse(world_scene: Node, d: Dictionary) -> void:
	var ent = _CORPSE_SCENE.instantiate()
	world_scene.add_child(ent)
	if ent.has_method("from_dict"):
		ent.from_dict(d)


func _spawn_grave_marker(world_scene: Node, d: Dictionary) -> void:
	var ent := Node2D.new()
	ent.set_script(_GRAVE_MARKER_SCRIPT)
	ent.name = "GraveMarker_%s" % str(d.get("pawn_name", "Unknown"))
	world_scene.add_child(ent)
	if ent.has_method("from_dict"):
		ent.from_dict(d)


func _spawn_grave_slot(world_scene: Node, d: Dictionary) -> void:
	var ent := Node2D.new()
	ent.set_script(_GRAVE_SLOT_SCRIPT)
	var tx: int = int(d.get("tile_x", 0))
	var ty: int = int(d.get("tile_y", 0))
	ent.name = "GraveSlot_%d_%d" % [tx, ty]
	world_scene.add_child(ent)
	if ent.has_method("setup"):
		ent.setup(Vector2i(tx, ty))
	if ent.has_method("from_dict"):
		ent.from_dict(d)


func _spawn_graveyard_zone(world_scene: Node, d: Dictionary) -> void:
	var ent := Node2D.new()
	ent.set_script(_GRAVEYARD_ZONE_SCRIPT)
	var rx: int = int(d.get("region_x", 0))
	var ry: int = int(d.get("region_y", 0))
	ent.name = "GraveyardZone_%d_%d" % [rx, ry]
	world_scene.add_child(ent)
	if ent.has_method("from_dict"):
		ent.from_dict(d)
	else:
		ent.region = Rect2i(rx, ry, int(d.get("region_w", 1)), int(d.get("region_h", 1)))
		ent.label  = str(d.get("label", "Graveyard"))
		ent.queue_redraw()


func _spawn_pawn(world_scene: Node, d: Dictionary) -> void:
	var ent: Pawn = _PAWN_SCENE.instantiate()
	world_scene.add_child(ent)

	# Attach JobRunner — mirrors world.gd's _spawn_sample_pawns pattern.
	if World.pathfinder != null:
		var jr := JobRunner.new()
		jr.name = "JobRunner"
		ent.add_child(jr)
		jr.setup(ent, World.pathfinder)
		ent.job_runner = jr

	# Minimal setup so tile / position is sane before from_dict overwrites everything.
	ent.setup(str(d.get("name", "Unknown")),
		Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))

	# Full state restore — overwrites setup() defaults.
	if ent.has_method("from_dict"):
		ent.from_dict(d)

	World.register_pawn(ent)


# ── tilemap helpers (defensive wrappers for Agent A's additions) ───────────────
# World.save_tilemap_layers() and World.apply_tilemap_layers() are added by
# Agent A in the Phase 16 parallel pass. has_method() guards keep this file
# clean when Agent A's work has not yet landed.

func _save_tilemap_layers_safe() -> Dictionary:
	if World.has_method("save_tilemap_layers"):
		return World.save_tilemap_layers()
	return {}


func _apply_tilemap_layers_safe(data: Dictionary) -> void:
	if World.has_method("apply_tilemap_layers"):
		World.apply_tilemap_layers(data)
	else:
		Audit.log("save", "apply_tilemap_layers: World helper not yet available — skipping")


# ── post-load reference resolution ────────────────────────────────────────────

## Resolve entity cross-references that require all nodes to be spawned first.
## Called once at the end of apply_save(), before load_finished is emitted.
##
## Beds — _pending_owner_name → _owner_pawn (Pawn node reference).
## Wolves — _pending_target_name → target_pawn (Pawn node reference).
func _post_load_resolve_references() -> void:
	# Build a name→pawn lookup once; both bed and wolf passes share it.
	var pawn_by_name: Dictionary = {}
	for pawn in World.pawns:
		if is_instance_valid(pawn):
			var n: String = str(pawn.get("pawn_name"))
			if n != "":
				pawn_by_name[n] = pawn

	# Beds: re-wire _owner_pawn from _pending_owner_name.
	var beds_resolved: int = 0
	for bed in World.beds:
		if not is_instance_valid(bed):
			continue
		var pending: String = str(bed.get("_pending_owner_name"))
		if pending == "":
			continue
		if pawn_by_name.has(pending):
			bed._owner_pawn = pawn_by_name[pending]
			beds_resolved += 1
		else:
			Audit.log("save", "bed at %s: owner pawn '%s' not found — left unowned" % [bed.tile, pending])
		bed._pending_owner_name = ""

	# Wolves: re-wire target_pawn from _pending_target_name.
	var wolves_resolved: int = 0
	for wolf in World.wolves:
		if not is_instance_valid(wolf):
			continue
		var pending: String = str(wolf.get("_pending_target_name"))
		if pending == "":
			continue
		if pawn_by_name.has(pending):
			wolf.target_pawn = pawn_by_name[pending]
			wolves_resolved += 1
		else:
			Audit.log("save", "wolf at %s: target pawn '%s' not found — will pick new target next tick" % [wolf.tile, pending])
		wolf._pending_target_name = ""

	if beds_resolved > 0 or wolves_resolved > 0:
		Audit.log("save", "_post_load_resolve_references: %d bed owners, %d wolf targets re-wired" % [beds_resolved, wolves_resolved])


# ── beauty / dirt helpers ──────────────────────────────────────────────────────

func _save_beauty_safe() -> Dictionary:
	if World.beauty_system != null and World.beauty_system.has_method("save_dict"):
		return World.beauty_system.save_dict()
	return {}


func _save_dirt_safe() -> Dictionary:
	if World.dirtiness_system != null and World.dirtiness_system.has_method("save_dict"):
		return World.dirtiness_system.save_dict()
	return {}


# ── file I/O ──────────────────────────────────────────────────────────────────

func _write_json(slot: StringName, payload: Dictionary) -> bool:
	var path := _path_for(slot)
	var f := FileAccess.open(path, FileAccess.WRITE)
	if f == null:
		push_error("SaveSystem._write_json: cannot open '%s' for writing" % path)
		return false
	f.store_string(JSON.stringify(payload, "\t"))
	return true


func _path_for(slot: StringName) -> String:
	return "user://save_%s.json" % String(slot)


# ── world scene locator ────────────────────────────────────────────────────────

## Returns the root Node2D of the World scene — the parent of all entity children.
## The World autoload is a data-only Node; the scene instance is a Node2D.
## Searches the scene tree root for a node named "World" first, then falls back
## to the first Node2D child of root (which is the active scene on most projects).
func _get_world_scene() -> Node:
	var root := get_tree().root
	var candidate := root.get_node_or_null("World")
	if candidate != null:
		return candidate
	# Fallback: first Node2D child of root.
	for child in root.get_children():
		if child is Node2D:
			return child
	push_error("SaveSystem._get_world_scene: cannot locate World scene node")
	return null
