# Signal operations module
# Handles: connect_signal_op, list_node_signals_op, list_signal_connections_op, disconnect_signal_op
# Corresponds to: test_signals.py, test_e2e_signals.py, test_signal_system.py
class_name SignalOps


## Connect a signal from one node to a method on another node
static func connect_signal_op(ctx: SceneTree, params: Dictionary) -> void:
	print("Connecting signal in scene: " + params.scene_path)
	
	# Ensure the scene path starts with res:// for Godot's resource system
	var full_scene_path: String = params.scene_path
	if not full_scene_path.begins_with("res://"):
		full_scene_path = "res://" + full_scene_path
	
	if ctx.debug_mode:
		print("Full scene path (with res://): " + full_scene_path)
		print("Source node path: " + params.source_node_path)
		print("Signal name: " + params.signal_name)
		print("Target node path: " + params.target_node_path)
		print("Method name: " + params.method_name)
	
	# Check if the scene file exists
	var scene_file_check: bool = FileAccess.file_exists(full_scene_path)
	if ctx.debug_mode:
		print("Scene file exists check: " + str(scene_file_check))
	
	if not scene_file_check:
		printerr("Scene file does not exist at: " + full_scene_path)
		var absolute_path: String = ProjectSettings.globalize_path(full_scene_path)
		printerr("Absolute file path that doesn't exist: " + absolute_path)
		ctx.quit(1)
	
	# Load the scene
	var scene: PackedScene = load(full_scene_path)
	if not scene:
		printerr("Failed to load scene: " + full_scene_path)
		ctx.quit(1)
	
	if ctx.debug_mode:
		print("Scene loaded successfully")
	
	# Instance the scene
	var scene_root: Node = scene.instantiate()
	if ctx.debug_mode:
		print("Scene instantiated")
	
	# Find the source node
	var source_node_path: String = params.source_node_path
	var source_node: Node = scene_root
	if source_node_path != "root" and source_node_path != "":
		var relative_path: String = source_node_path
		if relative_path.begins_with("root/"):
			relative_path = relative_path.substr(5)  # Remove "root/" prefix
		if ctx.debug_mode:
			print("Source node relative path: " + relative_path)
		source_node = scene_root.get_node(relative_path)
	
	if not source_node:
		printerr("Source node not found: " + source_node_path)
		ctx.quit(1)
	
	if ctx.debug_mode:
		print("Source node found: " + source_node.name + " of type: " + source_node.get_class())
	
	# Find the target node
	var target_node_path: String = params.target_node_path
	var target_node: Node = scene_root
	if target_node_path != "root" and target_node_path != "":
		var relative_path: String = target_node_path
		if relative_path.begins_with("root/"):
			relative_path = relative_path.substr(5)  # Remove "root/" prefix
		if ctx.debug_mode:
			print("Target node relative path: " + relative_path)
		target_node = scene_root.get_node(relative_path)
	
	if not target_node:
		printerr("Target node not found: " + target_node_path)
		ctx.quit(1)
	
	if ctx.debug_mode:
		print("Target node found: " + target_node.name + " of type: " + target_node.get_class())
	
	# Get signal and method names
	var signal_name: String = params.signal_name
	var method_name: String = params.method_name
	
	# Check if the source node has the signal
	var signal_list: Array = source_node.get_signal_list()
	var signal_exists: bool = false
	for sig in signal_list:
		if sig["name"] == signal_name:
			signal_exists = true
			break
	
	if not signal_exists:
		printerr("Signal '" + signal_name + "' does not exist on node: " + source_node.get_class())
		printerr("Available signals: " + str(signal_list.map(func(s): return s["name"])))
		ctx.quit(1)
	
	if ctx.debug_mode:
		print("Signal '" + signal_name + "' exists on source node")
	
	# Connect the signal with CONNECT_PERSIST flag (value 4) so it's saved to the scene file
	var callable: Callable = Callable(target_node, method_name)
	var connect_flags: int = CONNECT_PERSIST  # This flag ensures the connection is saved to the scene file
	
	# Check if already connected
	if source_node.is_connected(signal_name, callable):
		if ctx.debug_mode:
			print("Signal already connected, skipping")
		print("Signal '" + signal_name + "' already connected to " + method_name)
	else:
		var error: int = source_node.connect(signal_name, callable, connect_flags)
		if error != OK:
			printerr("Failed to connect signal: " + str(error))
			ctx.quit(1)
		if ctx.debug_mode:
			print("Signal connected successfully")
	
	# Pack and save the scene
	var packed_scene: PackedScene = PackedScene.new()
	var result: int = packed_scene.pack(scene_root)
	if ctx.debug_mode:
		print("Pack result: " + str(result) + " (OK=" + str(OK) + ")")
	
	if result == OK:
		if ctx.debug_mode:
			print("Saving scene to: " + full_scene_path)
		var error: int = ResourceSaver.save(packed_scene, full_scene_path)
		if ctx.debug_mode:
			print("Save result: " + str(error) + " (OK=" + str(OK) + ")")
		
		if error == OK:
			if ctx.debug_mode:
				var file_check_after: bool = FileAccess.file_exists(full_scene_path)
				print("File exists check after save: " + str(file_check_after))
				
				if file_check_after:
					print("Signal connected successfully: " + signal_name + " -> " + method_name)
					var absolute_path: String = ProjectSettings.globalize_path(full_scene_path)
					print("Absolute file path: " + absolute_path)
				else:
					printerr("File reported as saved but does not exist at: " + full_scene_path)
			else:
				print("Signal connected successfully: " + signal_name + " -> " + method_name)
		else:
			printerr("Failed to save scene: " + str(error))
	else:
		printerr("Failed to pack scene: " + str(result))


## List all signals available on a node
## @param ctx: SceneTree - The main script context
## @param params: Dictionary - Parameters from Python:
##   - scene_path: String - Path to the scene file
##   - node_path: String - Path to the node
static func list_node_signals_op(ctx: SceneTree, params: Dictionary) -> void:
	var scene_path: String = params.scene_path
	var node_path: String = params.node_path
	
	if ctx.debug_mode:
		print("[DEBUG] Listing signals for node: " + node_path)
	
	# Normalize scene path
	var full_scene_path: String = scene_path
	if not full_scene_path.begins_with("res://"):
		full_scene_path = "res://" + full_scene_path
	
	# Check if scene exists
	if not FileAccess.file_exists(full_scene_path):
		printerr("Scene file does not exist: " + full_scene_path)
		ctx.quit(1)
	
	# Load and instantiate scene
	var scene: PackedScene = load(full_scene_path)
	if not scene:
		printerr("Failed to load scene: " + full_scene_path)
		ctx.quit(1)
	
	var scene_root: Node = scene.instantiate()
	
	# Find target node
	var target_node: Node = _find_node(scene_root, node_path)
	if not target_node:
		printerr("Node not found: " + node_path)
		scene_root.queue_free()
		ctx.quit(1)
	
	# Get signal list
	var signal_list: Array = target_node.get_signal_list()
	var signals: Array = []
	
	for sig: Dictionary in signal_list:
		var signal_info: Dictionary = {
			"name": sig["name"],
			"args": [],
		}
		
		# Parse arguments if present
		if sig.has("args"):
			for arg: Dictionary in sig["args"]:
				signal_info["args"].append({
					"name": arg.get("name", ""),
					"type": _get_type_name(arg.get("type", TYPE_NIL)),
				})
		
		signals.append(signal_info)
	
	# Clean up
	scene_root.queue_free()
	
	# Output as JSON
	var result: Dictionary = {
		"node_name": target_node.name,
		"node_type": target_node.get_class(),
		"signals": signals,
		"count": signals.size(),
	}
	print(JSON.stringify(result))


## List signal connections in a scene or for a specific node
## @param ctx: SceneTree - The main script context
## @param params: Dictionary - Parameters from Python:
##   - scene_path: String - Path to the scene file
##   - node_path: String - Optional path to a specific node
static func list_signal_connections_op(ctx: SceneTree, params: Dictionary) -> void:
	var scene_path: String = params.scene_path
	var node_path: String = params.get("node_path", "")
	
	if ctx.debug_mode:
		print("[DEBUG] Listing signal connections in scene: " + scene_path)
		if not node_path.is_empty():
			print("[DEBUG] Filtering by node: " + node_path)
	
	# Normalize scene path
	var full_scene_path: String = scene_path
	if not full_scene_path.begins_with("res://"):
		full_scene_path = "res://" + full_scene_path
	
	# Check if scene exists
	if not FileAccess.file_exists(full_scene_path):
		printerr("Scene file does not exist: " + full_scene_path)
		ctx.quit(1)
	
	# Load and instantiate scene
	var scene: PackedScene = load(full_scene_path)
	if not scene:
		printerr("Failed to load scene: " + full_scene_path)
		ctx.quit(1)
	
	var scene_root: Node = scene.instantiate()
	
	# Collect connections
	var connections: Array = []
	
	if node_path.is_empty():
		# Get connections from all nodes
		_collect_connections_recursive(scene_root, scene_root, connections, ctx.debug_mode)
	else:
		# Get connections from specific node
		var target_node: Node = _find_node(scene_root, node_path)
		if not target_node:
			printerr("Node not found: " + node_path)
			scene_root.queue_free()
			ctx.quit(1)
		_collect_node_connections(target_node, scene_root, connections, ctx.debug_mode)
	
	# Clean up
	scene_root.queue_free()
	
	# Output as JSON
	var result: Dictionary = {
		"connections": connections,
		"count": connections.size(),
	}
	print(JSON.stringify(result))


## Disconnect a signal in a scene
## @param ctx: SceneTree - The main script context
## @param params: Dictionary - Parameters from Python:
##   - scene_path: String - Path to the scene file
##   - source_node_path: String - Path to the source node
##   - signal_name: String - Name of the signal
##   - target_node_path: String - Path to the target node
##   - method_name: String - Name of the method
static func disconnect_signal_op(ctx: SceneTree, params: Dictionary) -> void:
	var scene_path: String = params.scene_path
	var source_node_path: String = params.source_node_path
	var signal_name: String = params.signal_name
	var target_node_path: String = params.target_node_path
	var method_name: String = params.method_name
	
	if ctx.debug_mode:
		print("[DEBUG] Disconnecting signal: " + signal_name)
		print("[DEBUG] From: " + source_node_path + " to " + target_node_path + "." + method_name)
	
	# Normalize scene path
	var full_scene_path: String = scene_path
	if not full_scene_path.begins_with("res://"):
		full_scene_path = "res://" + full_scene_path
	
	# Check if scene exists
	if not FileAccess.file_exists(full_scene_path):
		printerr("Scene file does not exist: " + full_scene_path)
		ctx.quit(1)
	
	# Load and instantiate scene
	var scene: PackedScene = load(full_scene_path)
	if not scene:
		printerr("Failed to load scene: " + full_scene_path)
		ctx.quit(1)
	
	var scene_root: Node = scene.instantiate()
	
	# Find source and target nodes
	var source_node: Node = _find_node(scene_root, source_node_path)
	if not source_node:
		printerr("Source node not found: " + source_node_path)
		scene_root.queue_free()
		ctx.quit(1)
	
	var target_node: Node = _find_node(scene_root, target_node_path)
	if not target_node:
		printerr("Target node not found: " + target_node_path)
		scene_root.queue_free()
		ctx.quit(1)
	
	# Check if connection exists
	var callable: Callable = Callable(target_node, method_name)
	if not source_node.is_connected(signal_name, callable):
		printerr("Signal connection not found")
		scene_root.queue_free()
		ctx.quit(1)
	
	# Disconnect the signal
	source_node.disconnect(signal_name, callable)
	
	if ctx.debug_mode:
		print("[DEBUG] Signal disconnected")
	
	# Save scene
	var packed_scene: PackedScene = PackedScene.new()
	var result: int = packed_scene.pack(scene_root)
	
	if result != OK:
		printerr("Failed to pack scene: " + str(result))
		scene_root.queue_free()
		ctx.quit(1)
	
	var save_error: int = ResourceSaver.save(packed_scene, full_scene_path)
	if save_error != OK:
		printerr("Failed to save scene: " + str(save_error))
		scene_root.queue_free()
		ctx.quit(1)
	
	scene_root.queue_free()
	print("Signal disconnected successfully: " + signal_name + " from " + method_name)


# ============================================================================
# Helper Functions
# ============================================================================


## Find a node by path
static func _find_node(scene_root: Node, node_path: String) -> Node:
	if node_path == "root" or node_path.is_empty():
		return scene_root
	
	var search_path: String = node_path
	if search_path.begins_with("root/"):
		search_path = search_path.substr(5)
	
	return scene_root.get_node_or_null(search_path)


## Recursively collect signal connections from all nodes
static func _collect_connections_recursive(node: Node, scene_root: Node, connections: Array, debug_mode: bool) -> void:
	_collect_node_connections(node, scene_root, connections, debug_mode)
	
	for child: Node in node.get_children():
		_collect_connections_recursive(child, scene_root, connections, debug_mode)


## Collect signal connections from a single node
static func _collect_node_connections(node: Node, scene_root: Node, connections: Array, debug_mode: bool) -> void:
	var signal_list: Array = node.get_signal_list()
	
	for sig: Dictionary in signal_list:
		var signal_name: String = sig["name"]
		var conn_list: Array = node.get_signal_connection_list(signal_name)
		
		for conn: Dictionary in conn_list:
			var target: Object = conn.get("callable").get_object()
			var method: String = conn.get("callable").get_method()
			
			if target is Node:
				var target_node: Node = target
				var source_path: String = _get_node_path(node, scene_root)
				var target_path: String = _get_node_path(target_node, scene_root)
				
				connections.append({
					"source": source_path,
					"signal": signal_name,
					"target": target_path,
					"method": method,
				})
				
				if debug_mode:
					print("[DEBUG] Found connection: " + source_path + "." + signal_name + " -> " + target_path + "." + method)


## Get node path relative to scene root
static func _get_node_path(node: Node, scene_root: Node) -> String:
	if node == scene_root:
		return "root"
	
	var path: String = str(scene_root.get_path_to(node))
	return "root/" + path


## Get type name from type constant
static func _get_type_name(type_id: int) -> String:
	match type_id:
		TYPE_NIL: return "null"
		TYPE_BOOL: return "bool"
		TYPE_INT: return "int"
		TYPE_FLOAT: return "float"
		TYPE_STRING: return "String"
		TYPE_VECTOR2: return "Vector2"
		TYPE_VECTOR3: return "Vector3"
		TYPE_OBJECT: return "Object"
		TYPE_ARRAY: return "Array"
		TYPE_DICTIONARY: return "Dictionary"
		_: return "Variant"
