@tool
extends SceneTree

# =============================================================================
# validate_map.gd — Map Scene Structure Validator
# =============================================================================
# Validates that a competitive FPS map scene follows the required structure:
#   - CSG Combiner as root node
#   - Spawn groups (T Spawn / CT Spawn)
#   - Bomb sites (A / B)
#   - Buy zones (T / CT)
#   - Map bounds (invisible walls / kill plane)
#   - LightmapGI configuration
#   - Lighting setup (WorldEnvironment, directional/ambient lights)
#
# Usage:
#   godot --headless --script client/scripts/validate_map.gd -- res://path/to/map.tscn
#
# Exit codes:
#   0  — All checks passed
#   1  — One or more checks failed
#   2  — Scene file not found at the given path
# =============================================================================

const EXPECTED_MIN_NODES := 15
const EXPECTED_MAX_NODES := 500

# Separator used to mark user arguments after --
const ARG_SEPARATOR := "--"

var _pass_count := 0
var _fail_count := 0
var _warning_count := 0
var _scene_path := ""
var _scene_instance: Node = null
var _root_node: Node = null


func _init() -> void:
	_parse_arguments()

	if _scene_path.is_empty():
		printerr("ERROR: No scene path provided. Usage: godot --headless --script validate_map.gd -- res://path/to/map.tscn")
		quit(2)
		return

	print("")
	print("==========================================")
	print("  MAP SCENE STRUCTURE VALIDATOR")
	print("  Scene: ", _scene_path)
	print("==========================================")
	print("")

	# Step 1 — Load and instantiate the scene
	var scene: PackedScene = _load_scene(_scene_path)
	if scene == null:
		printerr("ERROR: Scene not found or failed to load: ", _scene_path)
		quit(2)
		return

	_scene_instance = _instantiate_scene(scene)
	if _scene_instance == null:
		printerr("ERROR: Failed to instantiate scene: ", _scene_path)
		quit(2)
		return

	root.add_child(_scene_instance)
	_root_node = _scene_instance

	# Step 2 — Run all validation checks
	_run_all_checks()

	# Step 3 — Print summary
	_print_summary()

	# Step 4 — Exit with appropriate code
	if _fail_count > 0:
		quit(1)
	else:
		quit(0)


# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------

func _parse_arguments() -> void:
	"""
	Parse command-line arguments to extract the scene path.
	Args after '--' are user arguments: godot ... --script validate_map.gd -- res://map.tscn
	"""
	var args: PackedStringArray = OS.get_cmdline_user_args()
	if args.is_empty():
		# Fallback: look for '--' in full cmdline
		var full_args: PackedStringArray = OS.get_cmdline_args()
		var found_sep := false
		for i in range(full_args.size()):
			if full_args[i] == ARG_SEPARATOR:
				found_sep = true
				continue
			if found_sep:
				_scene_path = full_args[i].strip_edges()
				break
	else:
		# First user arg is the scene path
		_scene_path = args[0].strip_edges()

	# Basic validation
	if _scene_path.is_empty():
		return

	# Ensure it's a res:// path or convert relative to res://
	if not _scene_path.begins_with("res://"):
		# Could be a relative filesystem path — keep as-is, ResourceLoader handles it
		pass


# ---------------------------------------------------------------------------
# Scene loading helpers
# ---------------------------------------------------------------------------

func _load_scene(path: String) -> PackedScene:
	"""
	Load a .tscn or .scn file and return the PackedScene, or null on failure.
	"""
	if not ResourceLoader.exists(path):
		# Try adding extension
		if not path.ends_with(".tscn") and not path.ends_with(".scn"):
			path = path + ".tscn"
		if not ResourceLoader.exists(path):
			return null
	var scene: PackedScene = ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)
	return scene


func _instantiate_scene(scene: PackedScene) -> Node:
	"""
	Safely instantiate a packed scene, returning the root node or null.
	"""
	var instance: Node = null
	var ok := false
	instance = scene.instantiate()
	if instance != null:
		ok = true
	return instance if ok else null


# ---------------------------------------------------------------------------
# Check registration helpers
# ---------------------------------------------------------------------------

func _pass(msg: String) -> void:
	"""Record a passed check."""
	_pass_count += 1
	print("  ✓ ", msg)


func _fail(msg: String) -> void:
	"""Record a failed check."""
	_fail_count += 1
	print("  ✗ ", msg)


func _warn(msg: String) -> void:
	"""Record a non-blocking warning."""
	_warning_count += 1
	print("  ⚠ ", msg)


func _check(condition: bool, msg_pass: String, msg_fail: String) -> bool:
	"""Convenience: pass or fail based on condition."""
	if condition:
		_pass(msg_pass)
	else:
		_fail(msg_fail)
	return condition


# ---------------------------------------------------------------------------
# Node search helpers
# ---------------------------------------------------------------------------

func _find_node(parent: Node, name: String) -> Node:
	"""Recursively find a node by exact name."""
	for child in parent.get_children():
		if child.name == name:
			return child
		var found := _find_node(child, name)
		if found != null:
			return found
	return null


func _find_node_by_type(parent: Node, type_name: String) -> Node:
	"""Recursively find a node by its script or class name."""
	for child in parent.get_children():
		var class_str := str(child.get_class())
		if class_str == type_name:
			return child
		var found := _find_node_by_type(child, type_name)
		if found != null:
			return found
	return null


func _find_nodes_by_type(parent: Node, type_name: String) -> Array[Node]:
	"""Recursively find all nodes of a given type."""
	var results: Array[Node] = []
	if str(parent.get_class()) == type_name:
		results.append(parent)
	for child in parent.get_children():
		results.append_array(_find_nodes_by_type(child, type_name))
	return results


func _find_children_by_name_prefix(parent: Node, prefix: String) -> Array[Node]:
	"""Find all immediate children whose name starts with prefix."""
	var results: Array[Node] = []
	for child in parent.get_children():
		if child.name.begins_with(prefix):
			results.append(child)
	return results


func _count_nodes(node: Node) -> int:
	"""Count total nodes in the subtree, including node itself."""
	var count := 1
	for child in node.get_children():
		count += _count_nodes(child)
	return count


# ---------------------------------------------------------------------------
# Main validation runner
# ---------------------------------------------------------------------------

func _run_all_checks() -> void:
	"""Execute all validation categories."""
	print("--- Root Node ---")
	_check_root_node()

	print("")
	print("--- Spawn Groups ---")
	_check_spawn_groups()

	print("")
	print("--- Bomb Sites ---")
	_check_bomb_sites()

	print("")
	print("--- Buy Zones ---")
	_check_buy_zones()

	print("")
	print("--- Map Bounds ---")
	_check_map_bounds()

	print("")
	print("--- LightmapGI Configuration ---")
	_check_lightmapgi()

	print("")
	print("--- Lighting Setup ---")
	_check_lighting()

	print("")
	print("--- Node Count Sanity ---")
	_check_node_sanity()

	print("")
	print("--- CSG Combiner Hierarchy ---")
	_check_csg_hierarchy()


# ---------------------------------------------------------------------------
# Individual check groups
# ---------------------------------------------------------------------------

func _check_root_node() -> void:
	"""Verify root node is a CSG Combiner or suitable root."""
	if _root_node == null:
		_fail("Root node is null")
		return

	var class_name := _root_node.get_class()
	# Accept CSGCombiner3D as root, or any reasonable Spatial/Node3D
	var is_csg := class_name in ["CSGCombiner3D", "CSGSphere3D", "CSGBox3D", "CSGCylinder3D", "CSGTorus3D"]
	var is_node3d := class_name in ["Node3D", "Spatial"]

	if is_csg:
		_pass("Root node is CSGCombiner3D ('" + _root_node.name + "')")
	elif is_node3d:
		_warn("Root node is Node3D ('" + _root_node.name + "') — expected CSGCombiner3D for map geometry")
	else:
		_warn("Root node is '" + class_name + "' ('" + _root_node.name + "') — expected CSGCombiner3D")


func _check_spawn_groups() -> void:
	"""Verify spawn point groups exist (T Spawn, CT Spawn)."""
	var found_t_spawn := false
	var found_ct_spawn := false

	for child in _root_node.get_children():
		var name_lower := child.name.to_lower()
		if "t_spawn" in name_lower or "tspawn" in name_lower or "terrorist" in name_lower:
			found_t_spawn = true
			_pass("T Spawn group found: '" + child.name + "'")
		if "ct_spawn" in name_lower or "ctspawn" in name_lower or "counter" in name_lower:
			found_ct_spawn = true
			_pass("CT Spawn group found: '" + child.name + "'")

	if not found_t_spawn:
		_fail("T Spawn group not found (expected node containing 'T_Spawn' or 'Terrorist')")
	if not found_ct_spawn:
		_fail("CT Spawn group not found (expected node containing 'CT_Spawn' or 'Counter')")


func _check_bomb_sites() -> void:
	"""Verify bomb site marker nodes exist (A, B)."""
	var found_a := false
	var found_b := false

	for child in _root_node.get_children():
		var name_lower := child.name.to_lower()
		if "bombsite_a" in name_lower or "bomb_site_a" in name_lower or "site_a" in name_lower or "a_site" in name_lower:
			found_a = true
			_pass("Bomb Site A found: '" + child.name + "'")
		if "bombsite_b" in name_lower or "bomb_site_b" in name_lower or "site_b" in name_lower or "b_site" in name_lower:
			found_b = true
			_pass("Bomb Site B found: '" + child.name + "'")

	if not found_a:
		_fail("Bomb Site A not found (expected node containing 'Bombsite_A' or 'Site_A')")
	if not found_b:
		_fail("Bomb Site B not found (expected node containing 'Bombsite_B' or 'Site_B')")


func _check_buy_zones() -> void:
	"""Verify buy zone areas exist for T and CT spawns."""
	var found_t_buy := false
	var found_ct_buy := false

	for child in _root_node.get_children():
		var name_lower := child.name.to_lower()
		if ("t_buy" in name_lower or "buy_t" in name_lower or "buyzone_t" in name_lower) and "zone" in name_lower:
			found_t_buy = true
			_pass("T Buy Zone found: '" + child.name + "'")
		if ("ct_buy" in name_lower or "buy_ct" in name_lower or "buyzone_ct" in name_lower) and "zone" in name_lower:
			found_ct_buy = true
			_pass("CT Buy Zone found: '" + child.name + "'")

	if not found_t_buy or not found_ct_buy:
		_warn("Buy zones not found — expected Area3D nodes named 'T_BuyZone' and 'CT_BuyZone'")


func _check_map_bounds() -> void:
	"""Verify map bounds exist (invisible walls, kill plane)."""
	var found_bounds := false
	var found_killplane := false

	for child in _root_node.get_children():
		var name_lower := child.name.to_lower()
		if "bounds" in name_lower or "boundary" in name_lower or "wall" in name_lower:
			if "invisible" in name_lower or "clip" in name_lower or "world" in name_lower:
				found_bounds = true
				_pass("Map bounds found: '" + child.name + "'")
		if "kill" in name_lower or "death" in name_lower or "fall" in name_lower:
			found_killplane = true
			_pass("Kill plane / death zone found: '" + child.name + "'")

	if not found_bounds:
		_warn("No map boundary/invisible wall nodes detected (optional but recommended)")
	if not found_killplane:
		_warn("No kill plane / death zone found (optional but recommended)")


func _check_lightmapgi() -> void:
	"""Validate LightmapGI node configuration."""
	var lightmap := _find_node_by_type(_root_node, "LightmapGI")
	if lightmap == null:
		_fail("LightmapGI node not found in scene")
		return

	_pass("LightmapGI node found: '" + lightmap.name + "'")

	# Check quality
	if lightmap.has_method("get_quality"):
		var q := lightmap.get_quality()
		_check(q >= 1, "LightmapGI quality set (≥ Medium)", "LightmapGI quality is Low (0) — set to at least Medium")

	# Check bounces
	var bounces: int = lightmap.get("bounces") if "bounces" in lightmap else -1
	if bounces >= 0:
		_check(bounces >= 2, "LightmapGI.bounces >= 2", "LightmapGI.bounces is " + str(bounces) + " — expected ≥ 2")

	# Check interior
	var interior: bool = lightmap.get("interior") if "interior" in lightmap else false
	_check(interior == true, "LightmapGI.interior = true (indoor map)", "LightmapGI.interior is false — set to true for indoor scenes")

	# Check texel scale
	var texel_scale: float = lightmap.get("texel_scale") if "texel_scale" in lightmap else 0.0
	_check(abs(texel_scale - 1.0) < 0.01, "LightmapGI.texel_scale ≈ 1.0", "LightmapGI.texel_scale is " + str(texel_scale) + " — expected 1.0")

	# Check denoiser
	var denoiser: bool = lightmap.get("use_denoiser") if "use_denoiser" in lightmap else false
	_check(denoiser == true, "LightmapGI.use_denoiser = true", "LightmapGI.use_denoiser is false — recommended true")

	# Check max texture size
	var max_tex: int = lightmap.get("max_texture_size") if "max_texture_size" in lightmap else 0
	if max_tex > 0:
		_check(max_tex <= 1024, "LightmapGI.max_texture_size ≤ 1024", "LightmapGI.max_texture_size is " + str(max_tex) + " — consider ≤ 1024")

	# Check energy
	var energy: float = lightmap.get("energy") if "energy" in lightmap else 1.0
	_warn("LightmapGI energy = " + str(energy) + " (verify visually)")


func _check_lighting() -> void:
	"""Verify lighting setup: WorldEnvironment, directional light, etc."""
	var world_env := _find_node_by_type(_root_node, "WorldEnvironment")
	if world_env != null:
		_pass("WorldEnvironment node found")
		# Check environment resource
		if world_env.has_method("get_environment"):
			var env := world_env.get_environment()
			if env != null:
				_pass("WorldEnvironment has Environment resource")
			else:
				_fail("WorldEnvironment has no Environment resource assigned")
		else:
			_warn("WorldEnvironment — check environment assignment manually")
	else:
		_fail("WorldEnvironment node not found — required for map lighting")

	# Directional light (sun)
	var sun := _find_node_by_type(_root_node, "DirectionalLight3D")
	if sun != null:
		_pass("DirectionalLight3D (sun) found: '" + sun.name + "'")
	else:
		_fail("DirectionalLight3D not found — map needs a sun light")

	# Check for too many dynamic lights
	var omni_lights := _find_nodes_by_type(_root_node, "OmniLight3D")
	var spot_lights := _find_nodes_by_type(_root_node, "SpotLight3D")
	if omni_lights.size() + spot_lights.size() > 4:
		_warn("Scene has " + str(omni_lights.size() + spot_lights.size()) + " dynamic lights — consider baking (recommended ≤ 4)")
	else:
		_pass("Dynamic light count (" + str(omni_lights.size() + spot_lights.size()) + ") within budget (≤ 4)")

	# Reflection probes
	var ref_probes := _find_nodes_by_type(_root_node, "ReflectionProbe")
	if ref_probes.size() > 0:
		_pass("ReflectionProbe(s) found: " + str(ref_probes.size()))
	else:
		_warn("No ReflectionProbe found — consider adding for visual quality")


func _check_node_sanity() -> void:
	"""Verify total node count is within expected range."""
	var total := _count_nodes(_root_node)
	if total < EXPECTED_MIN_NODES:
		_warn("Scene has only " + str(total) + " nodes — very minimal (< " + str(EXPECTED_MIN_NODES) + ")")
	elif total > EXPECTED_MAX_NODES:
		_warn("Scene has " + str(total) + " nodes — consider optimization (> " + str(EXPECTED_MAX_NODES) + ")")
	else:
		_pass("Node count " + str(total) + " within expected range [" + str(EXPECTED_MIN_NODES) + ", " + str(EXPECTED_MAX_NODES) + "]")


func _check_csg_hierarchy() -> void:
	"""Check that CSG nodes are properly parented under a CSGCombiner3D."""
	var csg_nodes := _find_nodes_by_type(_root_node, "CSGCombiner3D")
	if csg_nodes.size() > 1:
		_warn("Multiple CSGCombiner3D nodes found (" + str(csg_nodes.size()) + ") — expected single root combiner")
	elif csg_nodes.size() == 0:
		_warn("No CSGCombiner3D found at root (map may use mesh instances instead)")

	var csg_shape_nodes := 0
	csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGBox3D").size()
	csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGSphere3D").size()
	csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGCylinder3D").size()
	csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGTorus3D").size()
	csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGPolygon3D").size()

	if csg_shape_nodes > 0:
		_pass("CSG primitive nodes found: " + str(csg_shape_nodes))
	else:
		_warn("No CSG primitive nodes found — map may use imported meshes instead")


# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------

func _print_summary() -> void:
	"""Print a final summary of all checks."""
	print("")
	print("==========================================")
	print("  VALIDATION SUMMARY")
	print("==========================================")
	print("  Scene:      ", _scene_path)
	print("  Passed:     ", _pass_count)
	print("  Failed:     ", _fail_count)
	print("  Warnings:   ", _warning_count)
	print("------------------------------------------")
	if _fail_count > 0:
		print("  RESULT:     FAILED  ❌")
		printerr("Validation FAILED — ", _fail_count, " check(s) failed.")
	else:
		print("  RESULT:     PASSED  ✅")
	print("==========================================")
	print("")
