Editor/Commands/Tools/SetComponentPropertiesTool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Editor;
using Sandbox;

namespace Braxnet.Commands.Tools;

[MCPTool( "set_component_properties", "Set Component Properties",
	"Set one or more properties of a component on a GameObject" )]
public class SetComponentPropertiesTool : IMCPTool
{
	public string Name => "set_component_properties";
	public string Title => "Set Component Properties";
	public string Description => "Set one or more properties of a component on a GameObject";

	public JsonElement InputSchema => JsonSerializer.SerializeToElement( new
	{
		type = "object",
		properties = new
		{
			gameObjectId = new { type = "string", description = "ID of the GameObject" },
			componentId = new { type = "string", description = "ID of the component" },
			properties = new
			{
				type = "array",
				description = "Array of properties to set",
				items = new
				{
					type = "object",
					properties = new
					{
						name = new { type = "string", description = "Name of the property to set" },
						value =
							new { type = "string", description = "Value to set the property to" }
					},
					required = new[] { "name", "value" }
				},
				minItems = 1
			}
		},
		required = new[] { "gameObjectId", "componentId", "properties" }
	} );

	public JsonElement OutputSchema => default;

	public async Task<CallToolResult> ExecuteAsync( Dictionary<string, object> arguments, string sessionId )
	{
		var result = new CallToolResult();

		if ( arguments == null )
		{
			result.IsError = true;
			result.Content.Add( new TextContent { Text = "No arguments provided" } );
			return result;
		}

		var gameObjectIdStr = arguments.GetValueOrDefault( "gameObjectId" )?.ToString();
		var componentIdStr = arguments.GetValueOrDefault( "componentId" )?.ToString();
		var propertiesArg = arguments.GetValueOrDefault( "properties" );

		if ( string.IsNullOrEmpty( gameObjectIdStr ) || string.IsNullOrEmpty( componentIdStr ) ||
		     propertiesArg == null )
		{
			result.IsError = true;
			result.Content.Add( new TextContent { Text = "gameObjectId, componentId, and properties are required" } );
			return result;
		}

		// Parse properties array
		List<(string name, string value)> propertiesToSet;
		try
		{
			var propertiesJson = JsonSerializer.Serialize( propertiesArg );
			var propertiesArray = JsonSerializer.Deserialize<JsonElement[]>( propertiesJson );

			propertiesToSet = propertiesArray.Select( prop =>
			{
				var name = prop.GetProperty( "name" ).GetString();
				var value = prop.GetProperty( "value" ).GetString();
				return (name, value);
			} ).ToList();

			if ( !propertiesToSet.Any() )
			{
				result.IsError = true;
				result.Content.Add( new TextContent { Text = "At least one property must be specified" } );
				return result;
			}
		}
		catch ( Exception ex )
		{
			result.IsError = true;
			result.Content.Add( new TextContent { Text = $"Invalid properties format: {ex.Message}" } );
			return result;
		}

		try
		{
			if ( !Guid.TryParse( gameObjectIdStr, out var gameObjectId ) ||
			     !Guid.TryParse( componentIdStr, out var componentId ) )
			{
				result.IsError = true;
				result.Content.Add( new TextContent { Text = "Invalid GameObject or Component ID format" } );
				return result;
			}

			await GameTask.MainThread(); // Ensure this runs on the main thread

			var gameObject = SceneEditorSession.Active.Scene.Directory.FindByGuid( gameObjectId );
			if ( gameObject == null )
			{
				result.IsError = true;
				result.Content.Add( new TextContent { Text = $"GameObject not found: {gameObjectId}" } );
				return result;
			}

			var component = gameObject.Components.FirstOrDefault( c => c.Id == componentId );
			if ( component == null )
			{
				result.IsError = true;
				result.Content.Add( new TextContent { Text = $"Component not found: {componentId}" } );
				return result;
			}

			var componentProperties = EditorTypeLibrary.GetPropertyDescriptions( component ).ToList();
			var serializedObject = component.GetSerialized();
			var successfulProperties = new List<(string name, string value)>();
			var failedProperties = new List<(string name, string value, string error)>();

			using ( SceneEditorSession.Active.UndoScope( "Set Properties" )
				       .WithGameObjectChanges( gameObject, GameObjectUndoFlags.All ).Push() )
			{
				foreach ( var (propertyName, propertyValue) in propertiesToSet )
				{
					try
					{
						var property = componentProperties
							.FirstOrDefault( p => p.Name.Equals( propertyName, StringComparison.OrdinalIgnoreCase ) );

						if ( property == null )
						{
							failedProperties.Add( (propertyName, propertyValue,
								$"Property not found: {propertyName}") );
							continue;
						}

						if ( !serializedObject.TryGetProperty( propertyName, out var serializedProperty ) )
						{
							failedProperties.Add( (propertyName, propertyValue,
								$"Property '{propertyName}' not found as a SerializedProperty") );
							continue;
						}

						Log.Info(
							$"Setting property '{propertyName}' on component {component.GetType().Name} with value '{propertyValue}'" );

						serializedObject.NoteStartEdit( serializedProperty );

						object newValue = propertyValue;

						if ( property.PropertyType.IsAssignableFrom( typeof(Sandbox.Resource) ) ||
						     property.PropertyType.IsAssignableTo( typeof(Sandbox.Resource) ) )
						{
							var asset = AssetSystem.FindByPath( propertyValue );

							if ( asset != null )
							{
								Log.Info(
									$"Found asset for property '{propertyName}': {asset.RelativePath} (type: {asset.AssetType})" );
								newValue = asset.LoadResource();
							}
							else
							{
								Log.Info( $"No asset found for property '{propertyName}': {propertyValue}" );
							}
						}
						else
						{
							Log.Info( "Property is not a Resource type, using raw value" );
						}

						serializedProperty.SetValue( newValue );
						serializedObject.NoteFinishEdit( serializedProperty );
						serializedObject.NoteChanged( serializedProperty );
						serializedObject.OnPropertyChanged?.Invoke( serializedProperty );

						successfulProperties.Add( (propertyName, propertyValue) );
						Log.Info(
							$"Property '{propertyName}' set to '{propertyValue}' on component {component.GetType().Name}" );
					}
					catch ( Exception ex )
					{
						failedProperties.Add( (propertyName, propertyValue, $"Error setting property: {ex.Message}") );
						Log.Error(
							$"Error setting property '{propertyName}' on component {component.GetType().Name}: {ex.Message}" );
					}
				}
			}

			// Build result message
			var messages = new List<string>();

			if ( successfulProperties.Any() )
			{
				messages.Add(
					$"Successfully set {successfulProperties.Count} properties: {string.Join( ", ", successfulProperties.Select( p => p.name ) )}" );
			}

			if ( failedProperties.Any() )
			{
				messages.Add( $"Failed to set {failedProperties.Count} properties:" );
				foreach ( var (name, _, error) in failedProperties )
				{
					messages.Add( $"  - {name}: {error}" );
				}
			}

			if ( failedProperties.Any() && !successfulProperties.Any() )
			{
				result.IsError = true;
			}

			result.Content.Add( new TextContent { Text = string.Join( "\n", messages ) } );
			result.StructuredContent = new
			{
				gameObjectId = gameObject.Id,
				componentId = component.Id,
				successful = successfulProperties.Select( p => new { p.name, p.value } ).ToArray(),
				failed = failedProperties.Select( p => new { p.name, p.value, p.error } ).ToArray()
			};
		}
		catch ( Exception ex )
		{
			result.IsError = true;
			result.Content.Add( new TextContent { Text = $"Error setting component properties: {ex.Message}" } );
			return result;
		}

		return result;
	}
}