@tool
extends Node

## Inspects node properties and allows updating them, including Resource types.


func register(server: Node) -> void:
	server.register_handler("inspect_node", _inspect_node)
	server.register_handler("update_property", _update_property)
	server.register_handler("get_signals", _get_signals)
	server.register_handler("connect_signal", _connect_signal)
	server.register_handler("set_anchor", _set_anchor)
	server.register_handler("add_resource", _add_resource)


func _inspect_node(args: Dictionary) -> Dictionary:
	var node_path: String = args.get("path", "")
	if node_path.is_empty():
		return {"ok": false, "error": "Missing 'path' argument"}

	var root := EditorInterface.get_edited_scene_root()
	if root == null:
		return {"ok": false, "error": "No scene open"}

	var target: Node = root.get_node_or_null(node_path)
	if target == null:
		return {"ok": false, "error": "Node not found: %s" % node_path}

	var detail: String = args.get("detail_level", "full")
	var info: String
	if detail == "summary":
		info = _gather_node_summary(target)
	else:
		info = _gather_node_info(target)
	return {"ok": true, "type": "text", "result": info}


func _gather_node_info(node: Node) -> String:
	var lines: PackedStringArray = []
	lines.append("Node: %s" % node.name)
	lines.append("Type: %s" % node.get_class())
	lines.append("Path: %s" % str(node.get_path()))

	var script: Script = node.get_script()
	if script:
		lines.append("Script: %s" % script.resource_path)

	lines.append("")
	lines.append("Properties:")

	var prop_list := node.get_property_list()
	for prop in prop_list:
		if prop["usage"] & PROPERTY_USAGE_EDITOR == 0:
			continue
		var name: String = prop["name"]
		if name.begins_with("_") or name.begins_with("metadata/"):
			continue

		var value = node.get(name)
		var value_str := _format_value(value)
		lines.append("  %s = %s" % [name, value_str])

	var signals := node.get_signal_list()
	if signals.size() > 0:
		lines.append("")
		lines.append("Signals:")
		for sig in signals:
			var connections := node.get_signal_connection_list(sig["name"])
			if connections.size() > 0:
				lines.append("  %s → %d connection(s)" % [sig["name"], connections.size()])

	var groups := node.get_groups()
	if groups.size() > 0:
		lines.append("")
		lines.append("Groups: %s" % ", ".join(groups))

	return "\n".join(lines)


func _gather_node_summary(node: Node) -> String:
	var lines: PackedStringArray = []
	lines.append("Node: %s" % node.name)
	lines.append("Type: %s" % node.get_class())
	lines.append("Path: %s" % str(node.get_path()))
	var script: Script = node.get_script()
	if script:
		lines.append("Script: %s" % script.resource_path)
	# Transform properties only
	var summary_props: Array[String] = ["position", "rotation", "scale", "size", "visible"]
	var has_section := false
	for prop in node.get_property_list():
		var pname: String = prop["name"]
		if pname in summary_props:
			if not has_section:
				lines.append("")
				has_section = true
			lines.append("  %s = %s" % [pname, _format_value(node.get(pname))])
	return "\n".join(lines)


func _format_value(value) -> String:
	if value == null:
		return "null"
	if value is String:
		return '"%s"' % value
	if value is Vector2 or value is Vector3 or value is Color or value is Rect2:
		return str(value)
	if value is Resource:
		if value.resource_path != "":
			return "<%s> %s" % [value.get_class(), value.resource_path]
		return "<%s>" % value.get_class()
	if value is NodePath:
		return "NodePath(%s)" % str(value)
	return str(value)


# ------------------------------------------------------------------
# Update property — with Resource support
# ------------------------------------------------------------------

func _update_property(args: Dictionary) -> Dictionary:
	var node_path: String = args.get("path", "")
	var prop_name: String = args.get("property", "")
	var prop_value = args.get("value")

	if node_path.is_empty() or prop_name.is_empty():
		return {"ok": false, "error": "Missing 'path' or 'property' argument"}

	var root := EditorInterface.get_edited_scene_root()
	if root == null:
		return {"ok": false, "error": "No scene open"}

	var target: Node = root.get_node_or_null(node_path)
	if target == null:
		return {"ok": false, "error": "Node not found: %s" % node_path}

	var current = target.get(prop_name)
	var converted = _convert_value(prop_value, current, prop_name)

	if converted == null and prop_value != null:
		return {"ok": false, "error": "Could not convert value for property '%s'" % prop_name}

	target.set(prop_name, converted)
	return {"ok": true, "type": "text", "result": "Set %s.%s = %s" % [target.name, prop_name, str(converted)]}


func _convert_value(value, reference, prop_name: String = ""):
	# --- Resource creation from dict: {"type": "RectangleShape2D", "properties": {...}} ---
	if value is Dictionary and value.has("type"):
		return _create_resource(value)

	# --- Resource path: load existing resource ---
	if value is String and (value as String).begins_with("res://"):
		if not ResourceLoader.exists(value):
			push_warning("GolemServer: Resource not found: %s" % value)
			return null
		var res = load(value)
		if res != null:
			return res
		push_warning("GolemServer: Failed to load resource: %s" % value)
		return null

	# --- Standard type conversions ---
	if reference is Vector2 and value is Array:
		return Vector2(value[0], value[1])
	if reference is Vector3 and value is Array:
		return Vector3(value[0], value[1], value[2])
	if reference is Vector4 and value is Array:
		return Vector4(value[0], value[1], value[2], value[3])
	if reference is Rect2 and value is Array:
		return Rect2(value[0], value[1], value[2], value[3])
	if reference is Color:
		if value is String:
			return Color(value)
		if value is Array:
			return Color(value[0], value[1], value[2], value[3] if value.size() > 3 else 1.0)
	if reference is int:
		return int(value)
	if reference is float:
		return float(value)
	if reference is bool:
		return bool(value)
	if reference is String:
		return str(value)
	if reference is NodePath:
		return NodePath(str(value))

	# --- If reference is null but value is a dict with "type", try resource ---
	if reference == null and value is Dictionary and value.has("type"):
		return _create_resource(value)

	return value


func _create_resource(spec: Dictionary):
	"""Create a Resource from a spec dict: {"type": "ClassName", "properties": {"key": val}}."""
	var type_name: String = spec.get("type", "")
	if type_name.is_empty():
		return null

	if not ClassDB.class_exists(type_name):
		return null

	if not ClassDB.is_parent_class(type_name, "Resource"):
		return null

	var resource = ClassDB.instantiate(type_name)
	if resource == null:
		return null

	var properties: Dictionary = spec.get("properties", {})
	for key in properties:
		var val = properties[key]
		var current = resource.get(key)
		var converted = _convert_value(val, current, key)
		resource.set(key, converted)

	return resource


# ------------------------------------------------------------------
# Get signals — list signals and their connections for a node
# ------------------------------------------------------------------

func _get_signals(args: Dictionary) -> Dictionary:
	var node_path: String = args.get("path", "")
	if node_path.is_empty():
		return {"ok": false, "error": "Missing 'path' argument"}

	var root := EditorInterface.get_edited_scene_root()
	if root == null:
		return {"ok": false, "error": "No scene open"}

	var target: Node = root.get_node_or_null(node_path)
	if target == null:
		return {"ok": false, "error": "Node not found: %s" % node_path}

	var signals := target.get_signal_list()
	if signals.size() == 0:
		return {"ok": true, "type": "text", "result": "Node '%s' has no signals." % target.name}

	var lines: PackedStringArray = []
	for sig in signals:
		var sig_name: String = sig["name"]
		var sig_args: Array = sig.get("args", [])
		var arg_strs: PackedStringArray = []
		for a in sig_args:
			var type_str := _type_name(a.get("type", 0))
			arg_strs.append("%s: %s" % [a.get("name", "?"), type_str])
		lines.append("%s(%s)" % [sig_name, ", ".join(arg_strs)])

		var connections := target.get_signal_connection_list(sig_name)
		if connections.size() == 0:
			lines.append("  (no connections)")
		else:
			for conn in connections:
				var callable: Callable = conn["callable"]
				var target_obj = callable.get_object()
				var method_name: String = callable.get_method()
				var target_path: String = ""
				if target_obj is Node and root != null:
					target_path = str(root.get_path_to(target_obj))
				else:
					target_path = str(target_obj)
				var flags: int = conn.get("flags", 0)
				lines.append("  → %s.%s (flags: %d)" % [target_path, method_name, flags])

	return {"ok": true, "type": "text", "result": "\n".join(lines)}


func _type_name(type_id: int) -> String:
	match type_id:
		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_COLOR: return "Color"
		TYPE_OBJECT: return "Object"
		TYPE_ARRAY: return "Array"
		TYPE_DICTIONARY: return "Dictionary"
		TYPE_NODE_PATH: return "NodePath"
		_: return "Variant"


# ------------------------------------------------------------------
# Connect signal — connect a signal between two nodes
# ------------------------------------------------------------------

func _connect_signal(args: Dictionary) -> Dictionary:
	var source_path: String = args.get("source", "")
	var signal_name: String = args.get("signal_name", "")
	var target_path: String = args.get("target", "")
	var method: String = args.get("method", "")

	if source_path.is_empty() or signal_name.is_empty() or target_path.is_empty() or method.is_empty():
		return {"ok": false, "error": "Missing required argument(s). Need: source, signal_name, target, method"}

	var root := EditorInterface.get_edited_scene_root()
	if root == null:
		return {"ok": false, "error": "No scene open"}

	var source_node: Node = root.get_node_or_null(source_path)
	if source_node == null:
		return {"ok": false, "error": "Source node not found: %s" % source_path}

	var target_node: Node = root.get_node_or_null(target_path)
	if target_node == null:
		return {"ok": false, "error": "Target node not found: %s" % target_path}

	if not source_node.has_signal(signal_name):
		return {"ok": false, "error": "Signal '%s' does not exist on node '%s'" % [signal_name, source_node.name]}

	var cb := Callable(target_node, method)
	if source_node.is_connected(signal_name, cb):
		return {"ok": false, "error": "Signal '%s' is already connected to %s.%s" % [signal_name, target_path, method]}

	source_node.connect(signal_name, cb)
	return {"ok": true, "type": "text", "result": "Connected %s.%s → %s.%s" % [source_path, signal_name, target_path, method]}


# ------------------------------------------------------------------
# Set anchor — configure Control anchors/offsets in one call
# ------------------------------------------------------------------

const PRESET_MAP := {
	"full_rect": Control.PRESET_FULL_RECT,
	"center": Control.PRESET_CENTER,
	"center_top": Control.PRESET_CENTER_TOP,
	"center_bottom": Control.PRESET_CENTER_BOTTOM,
	"center_left": Control.PRESET_CENTER_LEFT,
	"center_right": Control.PRESET_CENTER_RIGHT,
	"top_left": Control.PRESET_TOP_LEFT,
	"top_right": Control.PRESET_TOP_RIGHT,
	"bottom_left": Control.PRESET_BOTTOM_LEFT,
	"bottom_right": Control.PRESET_BOTTOM_RIGHT,
	"top_wide": Control.PRESET_TOP_WIDE,
	"bottom_wide": Control.PRESET_BOTTOM_WIDE,
	"left_wide": Control.PRESET_LEFT_WIDE,
	"right_wide": Control.PRESET_RIGHT_WIDE,
	"hcenter_wide": Control.PRESET_HCENTER_WIDE,
	"vcenter_wide": Control.PRESET_VCENTER_WIDE,
}

func _set_anchor(args: Dictionary) -> Dictionary:
	var node_path: String = args.get("path", "")
	var preset_str: String = args.get("preset", "full_rect")
	var keep_offsets: bool = args.get("keep_offsets", false)

	if node_path.is_empty():
		return {"ok": false, "error": "Missing 'path' argument"}

	var root := EditorInterface.get_edited_scene_root()
	if root == null:
		return {"ok": false, "error": "No scene open"}

	var target: Node = root.get_node_or_null(node_path)
	if target == null:
		return {"ok": false, "error": "Node not found: %s" % node_path}

	if not target is Control:
		return {"ok": false, "error": "Node '%s' is not a Control (is %s)" % [target.name, target.get_class()]}

	if not PRESET_MAP.has(preset_str):
		var valid := ", ".join(PRESET_MAP.keys())
		return {"ok": false, "error": "Unknown preset '%s'. Valid: %s" % [preset_str, valid]}

	var preset_enum: int = PRESET_MAP[preset_str]

	if keep_offsets:
		(target as Control).set_anchors_preset(preset_enum)
	else:
		(target as Control).set_anchors_and_offsets_preset(preset_enum)

	return {"ok": true, "type": "text", "result": "Set anchor preset '%s' on %s%s" % [preset_str, target.name, " (kept offsets)" if keep_offsets else ""]}


# ------------------------------------------------------------------
# Add resource — create and save a standalone .tres file
# ------------------------------------------------------------------

func _add_resource(args: Dictionary) -> Dictionary:
	var type_name: String = args.get("type", "")
	var save_path: String = args.get("path", "")
	var properties: Dictionary = args.get("properties", {})

	if type_name.is_empty() or save_path.is_empty():
		return {"ok": false, "error": "Missing 'type' or 'path' argument"}

	if not save_path.begins_with("res://"):
		save_path = "res://" + save_path
	if not save_path.ends_with(".tres"):
		save_path += ".tres"

	if not ClassDB.class_exists(type_name):
		return {"ok": false, "error": "Unknown class: %s" % type_name}

	if not ClassDB.is_parent_class(type_name, "Resource"):
		return {"ok": false, "error": "'%s' is not a Resource subclass" % type_name}

	var resource = ClassDB.instantiate(type_name)
	if resource == null:
		return {"ok": false, "error": "Failed to instantiate %s" % type_name}

	for key in properties:
		var current = resource.get(key)
		var converted = _convert_value(properties[key], current, key)
		if converted == null and properties[key] != null:
			return {"ok": false, "error": "Could not convert value for property '%s'" % key}
		resource.set(key, converted)

	# Create directories if needed
	var dir_path := save_path.get_base_dir()
	if not DirAccess.dir_exists_absolute(dir_path):
		var err := DirAccess.make_dir_recursive_absolute(dir_path)
		if err != OK:
			return {"ok": false, "error": "Cannot create directory: %s (error %d)" % [dir_path, err]}

	var err := ResourceSaver.save(resource, save_path)
	if err != OK:
		return {"ok": false, "error": "Failed to save resource to %s (error %d)" % [save_path, err]}

	EditorInterface.get_resource_filesystem().scan()
	return {"ok": true, "type": "text", "result": "Created %s → %s" % [type_name, save_path]}
