Editor/Handlers/AssetHandler.cs
using Sandbox;
using SboxMcp;

namespace SboxMcp.Handlers;

/// <summary>
/// Handles cloud asset / local asset commands: asset.search, asset.fetch,
/// asset.mount, asset.browse_local.
/// </summary>
public static class AssetHandler
{
	/// <summary>
	/// asset.search — Search the s&box asset store for packages.
	/// Params: { "query": string, "type"?: string, "amount"?: string (default "20") }
	/// </summary>
	public static async Task<object> SearchAssets( HandlerRequest request )
	{
		var query  = GetParam( request, "query" );
		var amount = GetParamOptional( request, "amount" ) ?? "20";

		if ( !int.TryParse( amount, out var take ) || take <= 0 )
			take = 20;

		try
		{
			var findResult = await Package.FindAsync( query, take: take );

			var results = new List<object>();
			if ( findResult.Packages is not null )
			{
				foreach ( var pkg in findResult.Packages )
				{
					results.Add( new
					{
						ident       = pkg.FullIdent,
						title       = pkg.Title,
						description = pkg.Description,
						type        = pkg.TypeName ?? "",
						thumb       = pkg.Thumb,
					} );
				}
			}

			Log.Info( $"[MCP] asset.search '{query}' -> {results.Count} results" );
			return results;
		}
		catch ( Exception ex )
		{
			throw new InvalidOperationException( $"asset.search failed: {ex.Message}", ex );
		}
	}

	/// <summary>
	/// asset.fetch — Fetch metadata for a specific package by ident.
	/// Params: { "ident": string }
	/// </summary>
	public static async Task<object> FetchAsset( HandlerRequest request )
	{
		var ident = GetParam( request, "ident" );

		try
		{
			var pkg = await Package.Fetch( ident, false );
			if ( pkg is null )
				throw new KeyNotFoundException( $"Package not found: {ident}" );

			Log.Info( $"[MCP] asset.fetch '{ident}' ok" );
			return new
			{
				ident        = pkg.FullIdent,
				title        = pkg.Title,
				description  = pkg.Description,
				type         = pkg.TypeName ?? "",
				thumb        = pkg.Thumb,
				primaryAsset = pkg.GetMeta( "PrimaryAsset", "" ),
			};
		}
		catch ( Exception ex )
		{
			throw new InvalidOperationException( $"asset.fetch failed: {ex.Message}", ex );
		}
	}

	/// <summary>
	/// asset.mount — Mount a package by ident so its assets are available locally.
	/// Params: { "ident": string }
	/// </summary>
	public static async Task<object> MountAsset( HandlerRequest request )
	{
		var ident = GetParam( request, "ident" );

		try
		{
			var pkg = await Package.Fetch( ident, false );
			if ( pkg is null )
				throw new KeyNotFoundException( $"Package not found: {ident}" );

			await pkg.MountAsync();
			AddPackageReference( ident );

			var primary = pkg.GetMeta( "PrimaryAsset", "" );
			Log.Info( $"[MCP] asset.mount '{ident}' ok, primaryAsset={primary}" );
			return new
			{
				mounted      = true,
				ident        = pkg.FullIdent,
				primaryAsset = primary,
			};
		}
		catch ( Exception ex )
		{
			throw new InvalidOperationException( $"asset.mount failed: {ex.Message}", ex );
		}
	}

	/// <summary>
	/// asset.browse_local — Enumerate local project assets.
	/// Params: { "directory"?: string (default "/"), "extension"?: string (e.g. ".vmdl") }
	/// </summary>
	public static Task<object> BrowseLocalAssets( HandlerRequest request )
	{
		var directory = GetParamOptional( request, "directory" ) ?? "/";
		var extension = GetParamOptional( request, "extension" );

		try
		{
			// Editor.AssetSystem and Editor.Asset live in Sandbox.Tools, not linked
			// in the publish-wizard's library compile. Reach them reflectively so
			// this file compiles in any context; at runtime the types resolve fine.
			var assetSystemType = Type.GetType( "Editor.AssetSystem, Sandbox.Tools" )
				?? AppDomain.CurrentDomain.GetAssemblies()
					.Select( a => a.GetType( "Editor.AssetSystem" ) )
					.FirstOrDefault( t => t is not null );

			var results = new List<object>();
			if ( assetSystemType is null )
			{
				Log.Warning( "[MCP] asset.browse_local: Editor.AssetSystem not available in this context." );
				return Task.FromResult<object>( results );
			}

			var allProp = assetSystemType.GetProperty( "All", BindingFlags.Public | BindingFlags.Static );
			var assets = allProp?.GetValue( null ) as System.Collections.IEnumerable;
			if ( assets is null )
				return Task.FromResult<object>( results );

			foreach ( var asset in assets )
			{
				var path = asset.GetType().GetProperty( "Path" )?.GetValue( asset ) as string ?? "";

				if ( !string.IsNullOrEmpty( directory ) && directory != "/" )
				{
					var dir = directory.TrimEnd( '/' );
					if ( !path.StartsWith( dir, StringComparison.OrdinalIgnoreCase ) )
						continue;
				}

				if ( !string.IsNullOrEmpty( extension ) )
				{
					if ( !path.EndsWith( extension, StringComparison.OrdinalIgnoreCase ) )
						continue;
				}

				var name = asset.GetType().GetProperty( "Name" )?.GetValue( asset ) as string ?? "";
				var assetType = asset.GetType().GetProperty( "AssetType" )?.GetValue( asset )?.ToString() ?? "";

				results.Add( new { path, name, assetType } );
			}

			Log.Info( $"[MCP] asset.browse_local dir='{directory}' ext='{extension}' -> {results.Count} results" );
			return Task.FromResult<object>( results );
		}
		catch ( Exception ex )
		{
			throw new InvalidOperationException( $"asset.browse_local failed: {ex.Message}", ex );
		}
	}

	// -------------------------------------------------------------------------
	// Project reference helpers
	// -------------------------------------------------------------------------

	/// <summary>
	/// Adds a package ident to the project's PackageReferences in .sbproj so it
	/// auto-mounts on project load. Safe to call multiple times — skips duplicates.
	/// </summary>
	public static void AddPackageReference( string ident )
	{
		try
		{
			// Find .sbproj by searching the project root directory
			var assetsPath = Project.Current?.GetAssetsPath();
			if ( assetsPath is null ) return;

			var projectDir = System.IO.Path.GetDirectoryName( assetsPath.TrimEnd( '/', '\\' ) );
			if ( projectDir is null ) return;

			var sbprojFiles = System.IO.Directory.GetFiles( projectDir, "*.sbproj" );
			if ( sbprojFiles.Length == 0 ) return;

			var projectPath = sbprojFiles[0];

			var json = System.IO.File.ReadAllText( projectPath );
			var doc = System.Text.Json.JsonDocument.Parse( json );

			// Check if already present
			if ( doc.RootElement.TryGetProperty( "PackageReferences", out var refs ) )
			{
				foreach ( var item in refs.EnumerateArray() )
				{
					if ( item.GetString() == ident )
						return; // already referenced
				}
			}

			// Re-serialize with the new reference added
			var node = System.Text.Json.Nodes.JsonNode.Parse( json );
			var arr = node["PackageReferences"]?.AsArray();
			if ( arr is null )
			{
				arr = new System.Text.Json.Nodes.JsonArray();
				node["PackageReferences"] = arr;
			}
			arr.Add( ident );

			var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true };
			System.IO.File.WriteAllText( projectPath, node.ToJsonString( options ) );
			Log.Info( $"[MCP] Added '{ident}' to PackageReferences" );
		}
		catch ( Exception ex )
		{
			Log.Warning( $"[MCP] Could not add package reference: {ex.Message}" );
		}
	}

	// -------------------------------------------------------------------------
	// Param helpers
	// -------------------------------------------------------------------------

	private static string GetParam( HandlerRequest request, string key )
	{
		var val = GetParamOptional( request, key );
		if ( val is null )
			throw new ArgumentException( $"Missing required parameter: {key}" );
		return val;
	}

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