Editor/Handlers/FileHandler.cs
using Sandbox;
using System.IO;
using SboxMcp;

namespace SboxMcp.Handlers;

/// <summary>
/// Handles file and project commands: file.read, file.write, file.list, project.info.
/// </summary>
public static class FileHandler
{
	/// <summary>
	/// file.read — Read a file relative to the project root.
	/// Params: { "path": "relative/path/to/file.txt" }
	/// </summary>
	public static Task<object> ReadFile( HandlerRequest request )
	{
		var path = GetParam( request, "path" );

		// Try the s&box mounted filesystem first, fall back to System.IO.
		string content;
		try
		{
			content = Sandbox.FileSystem.Mounted.ReadAllText( path );
		}
		catch
		{
			var fullPath = ResolveAbsolutePath( path );
			content = File.ReadAllText( fullPath );
		}

		return Task.FromResult<object>( (object)new { path, content } );
	}

	/// <summary>
	/// file.write — Write content to a file relative to the project root.
	/// Params: { "path": "relative/path", "content": "file content" }
	/// </summary>
	public static Task<object> WriteFile( HandlerRequest request )
	{
		var path    = GetParam( request, "path" );
		var content = GetParam( request, "content" );

		// Write to the active project directory — code files go in code/, assets in Assets/
		var projectRoot = Project.Current?.GetRootPath();
		if ( projectRoot is null )
			throw new InvalidOperationException( "No active project found." );

		string baseDir;
		if ( path.EndsWith( ".cs", StringComparison.OrdinalIgnoreCase ) )
			baseDir = Path.Combine( projectRoot, "code" );
		else
			baseDir = Project.Current.GetAssetsPath();

		var fullPath = Path.Combine( baseDir, path );
		var dir = Path.GetDirectoryName( fullPath );
		if ( dir is not null )
			Directory.CreateDirectory( dir );
		File.WriteAllText( fullPath, content );

		Log.Info( $"[MCP] Wrote file: {fullPath}" );
		return Task.FromResult<object>( (object)new { path, written = true } );
	}

	/// <summary>
	/// file.list — List files in a directory, with optional glob pattern.
	/// Params: { "directory": "path/to/dir", "pattern"?: "*.cs" }
	/// </summary>
	public static Task<object> ListFiles( HandlerRequest request )
	{
		var directory = GetParam( request, "directory" );
		var pattern   = GetParamOptional( request, "pattern" ) ?? "*";

		var files = new List<string>();

		// Try s&box FileSystem first.
		try
		{
			var found = Sandbox.FileSystem.Mounted.FindFile( directory, pattern );
			files.AddRange( found );
		}
		catch
		{
			// Fall back to System.IO.
			var fullPath = ResolveAbsolutePath( directory );
			if ( Directory.Exists( fullPath ) )
			{
				foreach ( var f in Directory.GetFiles( fullPath, pattern, SearchOption.TopDirectoryOnly ) )
					files.Add( Path.GetRelativePath( fullPath, f ) );
			}
		}

		return Task.FromResult<object>( (object)new { directory, pattern, files } );
	}

	/// <summary>
	/// project.info — Return metadata about the current project.
	/// Params: (none)
	/// </summary>
	public static Task<object> ProjectInfo( HandlerRequest request )
	{
		string title       = Project.Current?.Config?.Title ?? "Unknown";
		string activeScene = "";
		int    objectCount = 0;

		try
		{
			// Editor session has the open .scene; Game.ActiveScene is play-mode only.
			var scene = EditorSession.ActiveScene ?? Game.ActiveScene;
			if ( scene is not null )
			{
				activeScene = scene.Name ?? "";
				foreach ( var _ in scene.GetAllObjects( false ) )
					objectCount++;
			}
		}
		catch { /* scene may be null */ }

		return Task.FromResult<object>( (object)new
		{
			title,
			activeScene,
			gameObjectCount = objectCount,
		} );
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------

	/// <summary>
	/// Resolves a relative path against the current working directory.
	/// </summary>
	private static string ResolveAbsolutePath( string relativePath )
	{
		string root;
		try
		{
			root = Directory.GetCurrentDirectory();
		}
		catch
		{
			root = ".";
		}

		return Path.Combine( root, relativePath );
	}

	private static string GetParam( HandlerRequest request, string key )
	{
		if ( request.Params is JsonElement el && el.TryGetProperty( key, out var prop ) )
		{
			var val = prop.GetString();
			if ( val is not null ) return val;
		}
		throw new ArgumentException( $"Missing required parameter: {key}" );
	}

	private static string GetParamOptional( HandlerRequest request, string key )
	{
		if ( request.Params is JsonElement el && el.TryGetProperty( key, out var prop ) )
			return prop.GetString();
		return null;
	}
}