@tool
extends Node
class_name ScriptTools
## Script and file management tools for MCP.
## Handles: edit_script, validate_script, list_scripts,
##          create_folder, delete_file, rename_file

var _editor_plugin: EditorPlugin = null

func set_editor_plugin(plugin: EditorPlugin) -> void:
	_editor_plugin = plugin

func _refresh_filesystem() -> void:
	if _editor_plugin:
		_editor_plugin.get_editor_interface().get_resource_filesystem().scan()

func _ensure_res_path(path: String) -> String:
	if not path.begins_with("res://"):
		return "res://" + path
	return path

# =============================================================================
# edit_script - Apply a small surgical code edit to a GDScript file
# =============================================================================
func edit_script(args: Dictionary) -> Dictionary:
	var edit: Dictionary = args.get(&"edit", {})
	if edit.is_empty():
		return {&"ok": false, &"error": "Missing 'edit' payload"}

	var path: String = str(edit.get(&"file", ""))
	if path.is_empty():
		return {&"ok": false, &"error": "Missing 'file' in edit"}

	path = _ensure_res_path(path)

	if not FileAccess.file_exists(path):
		return {&"ok": false, &"error": "File not found: " + path}

	var spec_type: String = str(edit.get(&"type", "snippet_replace"))
	if spec_type != "snippet_replace":
		return {&"ok": false, &"error": "Only 'snippet_replace' type is supported"}

	var old_snippet: String = str(edit.get(&"old_snippet", ""))
	var new_snippet: String = str(edit.get(&"new_snippet", ""))
	var context_before: String = str(edit.get(&"context_before", ""))
	var context_after: String = str(edit.get(&"context_after", ""))

	if old_snippet.is_empty():
		return {&"ok": false, &"error": "Missing 'old_snippet' in edit"}

	# Read current file content
	var file := FileAccess.open(path, FileAccess.READ)
	if not file:
		return {&"ok": false, &"error": "Cannot read file: " + path}
	var content := file.get_as_text()
	file.close()

	# Find and replace the snippet
	var search_text := old_snippet
	var pos := content.find(search_text)

	# If not found directly, try with context
	if pos == -1 and not context_before.is_empty():
		var ctx_pos := content.find(context_before)
		if ctx_pos != -1:
			var after_ctx := ctx_pos + context_before.length()
			var remaining := content.substr(after_ctx)
			var snippet_pos := remaining.find(old_snippet)
			if snippet_pos != -1:
				pos = after_ctx + snippet_pos

	if pos == -1:
		return {&"ok": false, &"error": "Could not find old_snippet in file. Make sure old_snippet matches the file content exactly."}

	# Check for multiple occurrences
	var second_pos := content.find(search_text, pos + 1)
	if second_pos != -1 and context_before.is_empty() and context_after.is_empty():
		return {&"ok": false, &"error": "old_snippet appears multiple times. Add context_before or context_after for disambiguation."}

	# Apply the replacement
	var original_content := content
	var new_content := content.substr(0, pos) + new_snippet + content.substr(pos + old_snippet.length())

	# Write back
	file = FileAccess.open(path, FileAccess.WRITE)
	if not file:
		return {&"ok": false, &"error": "Cannot write file: " + path}
	file.store_string(new_content)
	file.close()

	# Count changes
	var old_lines := old_snippet.split("\n")
	var new_lines := new_snippet.split("\n")
	var added := maxi(0, new_lines.size() - old_lines.size())
	var removed := maxi(0, old_lines.size() - new_lines.size())

	_refresh_filesystem()

	return {
		&"ok": true,
		&"path": path,
		&"added": added,
		&"removed": removed,
		&"auto_applied": true,
		&"message": "Applied edit to %s (+%d -%d lines)" % [path, added, removed]
	}

# =============================================================================
# validate_script
# =============================================================================
func validate_script(args: Dictionary) -> Dictionary:
	var path: String = str(args.get(&"path", ""))
	if path.strip_edges().is_empty():
		return {&"ok": false, &"error": "Missing 'path'"}

	path = _ensure_res_path(path)

	if not FileAccess.file_exists(path):
		return {&"ok": false, &"error": "File not found: " + path}

	# Read the source text directly from disk so we validate the *current*
	# file contents, not a stale resource-cache entry.
	var file := FileAccess.open(path, FileAccess.READ)
	if not file:
		return {&"ok": false, &"error": "Cannot read file: " + path}
	var source_code := file.get_as_text()
	file.close()

	# Create a fresh GDScript instance and assign the source for parsing.
	var script := GDScript.new()
	script.source_code = source_code

	# reload() triggers the parser/compiler and returns OK or an error code.
	var err := script.reload()

	if err != OK:
		# Try to extract useful details from the Godot output log.
		var errors := _collect_recent_script_errors(path)
		return {
			&"ok": true,
			&"valid": false,
			&"path": path,
			&"error_code": err,
			&"errors": errors,
			&"message": "Script has errors." + (" Details: " + "; ".join(errors) if errors.size() > 0 else " Check Godot console for details.")
		}

	if not script.can_instantiate():
		return {
			&"ok": true,
			&"valid": false,
			&"path": path,
			&"message": "Script parsed but cannot be instantiated (may have dependency errors)"
		}

	return {
		&"ok": true,
		&"valid": true,
		&"path": path,
		&"message": "No syntax errors found"
	}

func _collect_recent_script_errors(script_path: String) -> Array:
	"""Grab recent SCRIPT ERROR / Parse Error lines from the editor Output panel
	that mention the given script path.  Best-effort — returns [] if the panel
	cannot be accessed."""
	var errors: Array = []
	if not _editor_plugin:
		return errors

	# Find the editor's Output panel RichTextLabel
	var base := _editor_plugin.get_editor_interface().get_base_control()
	var editor_log := _find_node_by_class(base, "EditorLog")
	if not editor_log:
		return errors
	var rtl := _find_child_rtl(editor_log)
	if not rtl:
		return errors

	var text: String = rtl.get_parsed_text()
	var short_path := script_path.get_file()  # e.g. "player.gd"

	for line: String in text.split("\n"):
		line = line.strip_edges()
		if line.is_empty():
			continue
		if short_path in line or script_path in line:
			if line.begins_with("SCRIPT ERROR:") or line.begins_with("Parse Error:") \
				or line.begins_with("ERROR:") or line.begins_with("at:"):
				errors.append(line)

	# Keep only the last 10 relevant lines
	if errors.size() > 10:
		errors = errors.slice(errors.size() - 10)
	return errors

func _find_node_by_class(root: Node, cls_name: String) -> Node:
	if root.get_class() == cls_name:
		return root
	for child: Node in root.get_children():
		var found := _find_node_by_class(child, cls_name)
		if found:
			return found
	return null

func _find_child_rtl(node: Node) -> RichTextLabel:
	for child: Node in node.get_children():
		if child is RichTextLabel:
			return child
		var found := _find_child_rtl(child)
		if found:
			return found
	return null

# =============================================================================
# list_scripts
# =============================================================================
func list_scripts(args: Dictionary) -> Dictionary:
	var scripts: Array = []
	_collect_scripts("res://", scripts)

	return {
		&"ok": true,
		&"scripts": scripts,
		&"count": scripts.size()
	}

func _collect_scripts(path: String, out: Array) -> void:
	var dir := DirAccess.open(path)
	if dir == null:
		return

	dir.list_dir_begin()
	var name := dir.get_next()
	while name != "":
		if name.begins_with("."):
			name = dir.get_next()
			continue

		var full_path := path.path_join(name)
		if dir.current_is_dir():
			_collect_scripts(full_path, out)
		elif name.ends_with(".gd"):
			out.append(full_path)

		name = dir.get_next()
	dir.list_dir_end()

# =============================================================================
# create_folder
# =============================================================================
func create_folder(args: Dictionary) -> Dictionary:
	var path: String = str(args.get(&"path", ""))
	if path.strip_edges().is_empty():
		return {&"ok": false, &"error": "Missing 'path'"}

	path = _ensure_res_path(path)

	if DirAccess.dir_exists_absolute(path):
		return {&"ok": true, &"path": path, &"message": "Directory already exists"}

	var err := DirAccess.make_dir_recursive_absolute(path)
	if err != OK:
		return {&"ok": false, &"error": "Failed to create directory: " + str(err)}

	_refresh_filesystem()

	return {&"ok": true, &"path": path, &"message": "Directory created"}

# =============================================================================
# delete_file
# =============================================================================
func delete_file(args: Dictionary) -> Dictionary:
	var path: String = str(args.get(&"path", ""))
	var confirm: bool = bool(args.get(&"confirm", false))
	var create_backup: bool = bool(args.get(&"create_backup", true))

	if path.strip_edges().is_empty():
		return {&"ok": false, &"error": "Missing 'path'"}
	if not confirm:
		return {&"ok": false, &"error": "Must set confirm=true to delete"}

	path = _ensure_res_path(path)

	if not FileAccess.file_exists(path):
		return {&"ok": false, &"error": "File not found: " + path}

	# Create backup
	if create_backup:
		var backup_path := path + ".bak"
		DirAccess.copy_absolute(path, backup_path)

	var err := DirAccess.remove_absolute(path)
	if err != OK:
		return {&"ok": false, &"error": "Failed to delete file: " + str(err)}

	_refresh_filesystem()

	return {&"ok": true, &"path": path, &"message": "File deleted" + (" (backup created)" if create_backup else "")}

# =============================================================================
# rename_file
# =============================================================================
func rename_file(args: Dictionary) -> Dictionary:
	var old_path: String = str(args.get(&"old_path", ""))
	var new_path: String = str(args.get(&"new_path", ""))

	if old_path.strip_edges().is_empty():
		return {&"ok": false, &"error": "Missing 'old_path'"}
	if new_path.strip_edges().is_empty():
		return {&"ok": false, &"error": "Missing 'new_path'"}

	old_path = _ensure_res_path(old_path)
	new_path = _ensure_res_path(new_path)

	if not FileAccess.file_exists(old_path):
		return {&"ok": false, &"error": "File not found: " + old_path}
	if FileAccess.file_exists(new_path):
		return {&"ok": false, &"error": "Target already exists: " + new_path}

	# Ensure target directory exists
	var dir_path := new_path.get_base_dir()
	if not DirAccess.dir_exists_absolute(dir_path):
		DirAccess.make_dir_recursive_absolute(dir_path)

	var err := DirAccess.rename_absolute(old_path, new_path)
	if err != OK:
		return {&"ok": false, &"error": "Failed to rename: " + str(err)}

	_refresh_filesystem()

	return {&"ok": true, &"old_path": old_path, &"new_path": new_path,
		&"message": "Renamed %s to %s" % [old_path, new_path]}
