Editor/UtilityToolHandlers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Editor;
using Sandbox;

namespace SboxMcpServer;

/// <summary>
/// Utility MCP tools: get_asset_dependencies, batch_transform, copy_component, get_object_bounds.
/// </summary>
internal static class UtilityToolHandlers
	{
	private static readonly JsonSerializerOptions _json = new()
	{
		PropertyNamingPolicy   = JsonNamingPolicy.CamelCase,
		DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
	};

	// ── get_asset_dependencies ────────────────────────────────────────────────

	internal static object GetAssetDependencies( JsonElement args )
	{
		string path = OzmiumSceneHelpers.Get( args, "assetPath", (string)null );
		if ( string.IsNullOrEmpty( path ) )
			return OzmiumSceneHelpers.Txt( "Provide 'assetPath'." );

		path = OzmiumSceneHelpers.NormalizePath( path );
		var asset = AssetSystem.FindByPath( path );
		if ( asset == null )
			return OzmiumSceneHelpers.Txt( $"Asset not found: '{path}'." );

		try
		{
			var deps = new List<string>();
			CollectDependencies( asset, deps, new HashSet<string>() );

			if ( deps.Count == 0 )
				return OzmiumSceneHelpers.Txt( $"No dependencies found for '{path}'." );

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				asset         = path,
				dependencies  = deps
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	private static void CollectDependencies( Asset asset, List<string> result, HashSet<string> visited )
	{
		if ( !visited.Add( asset.Path ) ) return;

		try
		{
			foreach ( var dep in asset.GetReferences( false ) )
			{
				result.Add( dep?.Path ?? "?" );
				var depAsset = AssetSystem.FindByPath( dep?.Path ?? "" );
				if ( depAsset != null )
					CollectDependencies( depAsset, result, visited );
			}
		}
		catch { }
	}

	// ── batch_transform ─────────────────────────────────────────────────────

	internal static object BatchTransform( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		if ( !args.TryGetProperty( "ids", out var idsEl ) || idsEl.ValueKind != JsonValueKind.Array )
			return OzmiumSceneHelpers.Txt( "Provide 'ids' array." );

		if ( !args.TryGetProperty( "position", out var posEl ) || posEl.ValueKind != JsonValueKind.Object )
			return OzmiumSceneHelpers.Txt( "Provide 'position' object {x,y,z}." );

		try
		{
			float ox = 0, oy = 0, oz = 0;
			if ( posEl.TryGetProperty( "x", out var xp ) ) ox = xp.GetSingle();
			if ( posEl.TryGetProperty( "y", out var yp ) ) oy = yp.GetSingle();
			if ( posEl.TryGetProperty( "z", out var zp ) ) oz = zp.GetSingle();

			int count = 0;
			var errors = new List<string>();

			foreach ( var idEl in idsEl.EnumerateArray() )
			{
				var idStr = idEl.GetString();
				if ( !Guid.TryParse( idStr, out var guid ) )
				{
					errors.Add( $"Invalid GUID: {idStr}" );
					continue;
					}

				var go = OzmiumSceneHelpers.WalkAll( scene, true ).FirstOrDefault( g => g.Id == guid );
				if ( go == null )
				{
					errors.Add( $"Not found: {idStr}" );
					continue;
				}

				go.WorldPosition += new Vector3( ox, oy, oz );
				count++;
			}

			if ( errors.Count > 0 )
				return OzmiumSceneHelpers.Txt( $"Moved {count} objects. Errors: {string.Join( ", ", errors )}" );

			return OzmiumSceneHelpers.Txt( $"Moved {count} objects by ({ox}, {oy}, {oz})." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── copy_component ───────────────────────────────────────────────────────

	internal static object CopyComponent( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		string sourceId   = OzmiumSceneHelpers.Get( args, "sourceId", (string)null );
		string sourceName = OzmiumSceneHelpers.Get( args, "sourceName", (string)null );
		string targetId   = OzmiumSceneHelpers.Get( args, "targetId", (string)null );
		string targetName = OzmiumSceneHelpers.Get( args, "targetName", (string)null );
		string compType   = OzmiumSceneHelpers.Get( args, "componentType", (string)null );

		if ( string.IsNullOrEmpty( compType ) )
			return OzmiumSceneHelpers.Txt( "Provide 'componentType'." );

		var sourceGo = OzmiumSceneHelpers.FindGo( scene, sourceId, sourceName );
		if ( sourceGo == null ) return OzmiumSceneHelpers.Txt( "Source object not found." );

		var targetGo = OzmiumSceneHelpers.FindGo( scene, targetId, targetName );
		if ( targetGo == null ) return OzmiumSceneHelpers.Txt( "Target object not found." );

		var sourceComp = sourceGo.Components.GetAll().FirstOrDefault( c =>
			c.GetType().Name.IndexOf( compType, StringComparison.OrdinalIgnoreCase ) >= 0 );
		if ( sourceComp == null )
			return OzmiumSceneHelpers.Txt( $"Component '{compType}' not found on source '{sourceGo.Name}'." );

		try
		{
			// Use TypeLibrary to create the same component type on the target
			var td = OzmiumWriteHandlers.FindComponentTypeDescription( compType );
			if ( td == null )
				return OzmiumSceneHelpers.Txt( $"Component type '{compType}' not found in TypeLibrary." );

			var newComp = targetGo.Components.Create( td );
			return OzmiumSceneHelpers.Txt( $"Copied {sourceComp.GetType().Name} from '{sourceGo.Name}' to '{targetGo.Name}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── get_object_bounds ────────────────────────────────────────────────────

	internal static object GetObjectBounds( JsonElement args )
	{
		var scene = OzmiumSceneHelpers.ResolveScene();
		if ( scene == null ) return OzmiumSceneHelpers.Txt( "No active scene." );

		string id   = OzmiumSceneHelpers.Get( args, "id",   (string)null );
		string name = OzmiumSceneHelpers.Get( args, "name", (string)null );

		var go = OzmiumSceneHelpers.FindGo( scene, id, name );
		if ( go == null ) return OzmiumSceneHelpers.Txt( "Object not found." );

		try
		{
			BBox bounds = BBox.FromPositionAndSize( go.WorldPosition, 0.1f );

			// Try collider bounds first
			var collider = go.Components.GetAll().FirstOrDefault( c => c is Collider ) as Collider;
			if ( collider != null )
				bounds = collider.GetWorldBounds();

			// Try ModelRenderer bounds
			var modelRenderer = go.Components.GetAll().FirstOrDefault( c => c.GetType().Name.Contains( "ModelRenderer" ) );
			if ( modelRenderer != null )
			{
				var prop = modelRenderer.GetType().GetProperty( "Model" );
				if ( prop != null )
				{
					var model = prop.GetValue( modelRenderer ) as Model;
					if ( model != null && !model.IsError )
					{
						var bbox = model.Bounds;
						if ( bbox.Volume > 0 )
							bounds = bbox;
					}
				}
			}

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				id      = go.Id.ToString(),
				name    = go.Name,
				enabled = go.Enabled,
				mins    = OzmiumSceneHelpers.V3( bounds.Mins ),
				maxs    = OzmiumSceneHelpers.V3( bounds.Maxs )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── Schemas ─────────────────────────────────────────────────────────────

	private static Dictionary<string, object> S( string name, string desc, Dictionary<string, object> props, string[] req = null )
	{
		var schema = new Dictionary<string, object> { ["type"] = "object", ["properties"] = props };
		if ( req != null ) schema["required"] = req;
		return new Dictionary<string, object> { ["name"] = name, ["description"] = desc, ["inputSchema"] = schema };
	}

	internal static Dictionary<string, object> SchemaGetAssetDependencies => S( "get_asset_dependencies",
		"Returns all assets referenced by a given asset (materials, textures, etc.).",
		new Dictionary<string, object>
		{
			["assetPath"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Asset path to query." }
		},
		new[] { "assetPath" } );

	internal static Dictionary<string, object> SchemaBatchTransform => S( "batch_transform",
		"Applies a position offset to multiple objects at once.",
		new Dictionary<string, object>
		{
			["ids"]      = new Dictionary<string, object> { ["type"] = "array", ["description"] = "Array of GUIDs.", ["items"] = new Dictionary<string, object> { ["type"] = "string" } },
			["position"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Offset {x,y,z} to add to each object." }
		} );

	internal static Dictionary<string, object> SchemaCopyComponent => S( "copy_component",
		"Copies a component from one GameObject to another.",
		new Dictionary<string, object>
	{
			["sourceId"]       = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Source GO GUID or name." },
			["sourceName"]     = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Source GO exact name." },
			["targetId"]       = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Target GO GUID." },
			["targetName"]     = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Target GO name." },
			["componentType"]   = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Component type to copy." }
		},
		new[] { "sourceId", "targetId", "componentType" } );

	internal static Dictionary<string, object> SchemaGetObjectBounds => S( "get_object_bounds",
		"Returns the world-space bounding box of a GameObject.",
		new Dictionary<string, object>
	{
			["id"]   = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
			["name"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." }
	} );
}