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

namespace SboxMcpServer;

/// <summary>
/// Handlers for all scene-write MCP tools:
/// create_game_object, add_component, remove_component, set_component_property,
/// destroy_game_object, reparent_game_object, set_game_object_tags,
/// instantiate_prefab, save_scene, undo, redo.
/// </summary>
internal static class OzmiumWriteHandlers
{
	private static readonly JsonSerializerOptions _json = new()
	{
		PropertyNamingPolicy   = JsonNamingPolicy.CamelCase,
		DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
	};

	// ── create_game_object ──────────────────────────────────────────────────

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

		string name     = OzmiumSceneHelpers.Get( args, "name",     "New GameObject" );
		string parentId = OzmiumSceneHelpers.Get( args, "parentId", (string)null );

		try
		{
			var go = scene.CreateObject();
			go.Name = name;

			if ( !string.IsNullOrEmpty( parentId ) && Guid.TryParse( parentId, out var guid ) )
			{
				var parent = OzmiumSceneHelpers.WalkAll( scene, true ).FirstOrDefault( g => g.Id == guid );
				if ( parent != null ) go.SetParent( parent );
			}

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message = $"Created '{go.Name}'.",
				id      = go.Id.ToString(),
				path    = OzmiumSceneHelpers.GetObjectPath( go )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── add_component ───────────────────────────────────────────────────────

	internal static object AddComponent( 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 );
		string type = OzmiumSceneHelpers.Get( args, "componentType", (string)null );

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

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

		try
		{
			// Use TypeLibrary (indexed, fast) to find the component type
			var td = FindComponentTypeDescription( type );
			if ( td == null ) return OzmiumSceneHelpers.Txt( $"Component type '{type}' not found. Use the exact class name." );
			go.Components.Create( td );
			return OzmiumSceneHelpers.Txt( $"Added '{td.Name}' to '{go.Name}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── remove_component ────────────────────────────────────────────────────

	internal static object RemoveComponent( 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 );
		string type = OzmiumSceneHelpers.Get( args, "componentType", (string)null );

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

		var comp = go.Components.GetAll().FirstOrDefault( c =>
			c.GetType().Name.IndexOf( type ?? "", StringComparison.OrdinalIgnoreCase ) >= 0 );
		if ( comp == null ) return OzmiumSceneHelpers.Txt( $"No component '{type}' found on '{go.Name}'." );

		try
		{
			comp.Destroy();
			return OzmiumSceneHelpers.Txt( $"Removed '{comp.GetType().Name}' from '{go.Name}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── set_component_property ──────────────────────────────────────────────

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

		string id           = OzmiumSceneHelpers.Get( args, "id",            (string)null );
		string objName      = OzmiumSceneHelpers.Get( args, "name",          (string)null );
		string compType     = OzmiumSceneHelpers.Get( args, "componentType", (string)null );
		string propName     = OzmiumSceneHelpers.Get( args, "propertyName",  (string)null );

		if ( string.IsNullOrEmpty( propName ) ) return OzmiumSceneHelpers.Txt( "Provide 'propertyName'." );
		if ( !args.TryGetProperty( "value", out var valEl ) ) return OzmiumSceneHelpers.Txt( "Provide 'value'." );

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

		var comp = go.Components.GetAll().FirstOrDefault( c =>
			string.IsNullOrEmpty( compType ) ||
			c.GetType().Name.IndexOf( compType, StringComparison.OrdinalIgnoreCase ) >= 0 );
		if ( comp == null ) return OzmiumSceneHelpers.Txt( $"Component '{compType}' not found." );

		var prop = comp.GetType().GetProperty( propName,
			System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance );
		if ( prop == null ) return OzmiumSceneHelpers.Txt( $"Property '{propName}' not found on '{comp.GetType().Name}'." );
		if ( !prop.CanWrite ) return OzmiumSceneHelpers.Txt( $"Property '{propName}' is read-only." );

		try
		{
			object converted = ConvertJsonValue( valEl, prop.PropertyType );
			prop.SetValue( comp, converted );
			var readback = prop.GetValue( comp );
			return OzmiumSceneHelpers.Txt( $"Set '{comp.GetType().Name}.{propName}' = {readback}" );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error setting property: {ex.Message}" ); }
	}

	// ── destroy_game_object ─────────────────────────────────────────────────

	internal static object DestroyGameObject( 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." );

		var displayName = go.Name;
		try
		{
			go.Destroy();
			return OzmiumSceneHelpers.Txt( $"Destroyed '{displayName}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── reparent_game_object ────────────────────────────────────────────────

	internal static object ReparentGameObject( 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 );
		string parentId = OzmiumSceneHelpers.Get( args, "parentId", (string)null );

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

		try
		{
			if ( string.IsNullOrEmpty( parentId ) || parentId == "null" )
			{
				go.SetParent( null );
				return OzmiumSceneHelpers.Txt( $"Moved '{go.Name}' to scene root." );
			}

			if ( !Guid.TryParse( parentId, out var guid ) ) return OzmiumSceneHelpers.Txt( "Invalid parentId GUID." );
			var parent = OzmiumSceneHelpers.WalkAll( scene, true ).FirstOrDefault( g => g.Id == guid );
			if ( parent == null ) return OzmiumSceneHelpers.Txt( $"Parent '{parentId}' not found." );
			go.SetParent( parent );
			return OzmiumSceneHelpers.Txt( $"Moved '{go.Name}' under '{parent.Name}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── set_game_object_tags ────────────────────────────────────────────────

	internal static object SetGameObjectTags( 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
		{
			// set: replace all tags
			if ( args.TryGetProperty( "set", out var setEl ) && setEl.ValueKind == JsonValueKind.Array )
			{
				go.Tags.RemoveAll();
				foreach ( var t in setEl.EnumerateArray() ) go.Tags.Add( t.GetString() );
				return OzmiumSceneHelpers.Txt( $"Tags on '{go.Name}': {string.Join( ", ", go.Tags.TryGetAll() )}" );
			}
			// add/remove individual tags
			if ( args.TryGetProperty( "add", out var addEl ) && addEl.ValueKind == JsonValueKind.Array )
				foreach ( var t in addEl.EnumerateArray() ) go.Tags.Add( t.GetString() );
			if ( args.TryGetProperty( "remove", out var remEl ) && remEl.ValueKind == JsonValueKind.Array )
				foreach ( var t in remEl.EnumerateArray() ) go.Tags.Remove( t.GetString() );

			return OzmiumSceneHelpers.Txt( $"Tags on '{go.Name}': {string.Join( ", ", go.Tags.TryGetAll() )}" );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── instantiate_prefab ──────────────────────────────────────────────────

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

		string path     = OzmiumSceneHelpers.NormalizePath( OzmiumSceneHelpers.Get( args, "path", (string)null ) );
		float  x        = OzmiumSceneHelpers.Get( args, "x",        0f );
		float  y        = OzmiumSceneHelpers.Get( args, "y",        0f );
		float  z        = OzmiumSceneHelpers.Get( args, "z",        0f );
		string parentId = OzmiumSceneHelpers.Get( args, "parentId", (string)null );

		if ( string.IsNullOrEmpty( path ) ) return OzmiumSceneHelpers.Txt( "Provide 'path' (prefab asset path)." );

		try
		{
			// Verify the asset exists
			var asset = AssetSystem.FindByPath( path );
			if ( asset == null )
				return OzmiumSceneHelpers.Txt( $"Asset not found: '{path}'. Use browse_assets with type='prefab' to find valid prefab paths." );

			var prefabFile = ResourceLibrary.Get<PrefabFile>( path );
			if ( prefabFile == null )
				return OzmiumSceneHelpers.Txt( $"Could not load prefab: '{path}'." );

			var prefabScene = SceneUtility.GetPrefabScene( prefabFile );
			var go = prefabScene.Clone();
			go.WorldPosition = new Vector3( x, y, z );

			if ( !string.IsNullOrEmpty( parentId ) && Guid.TryParse( parentId, out var guid ) )
			{
				var parent = OzmiumSceneHelpers.WalkAll( scene, true ).FirstOrDefault( g => g.Id == guid );
				if ( parent != null ) go.SetParent( parent );
			}

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message  = $"Instantiated '{path}'.",
				id       = go.Id.ToString(),
				name     = go.Name,
				position = OzmiumSceneHelpers.V3( go.WorldPosition )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── save_scene ──────────────────────────────────────────────────────────

	internal static object SaveScene()
	{
		try
		{
			var session = SceneEditorSession.Active;
			if ( session == null ) return OzmiumSceneHelpers.Txt( "No editor session active." );
			session.Save( false );
			return OzmiumSceneHelpers.Txt( $"Saved '{session.Scene?.Name}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error saving: {ex.Message}" ); }
	}

	// ── undo / redo ─────────────────────────────────────────────────────────

	internal static object Undo()
	{
		try
		{
			var session = SceneEditorSession.Active;
			if ( session == null ) return OzmiumSceneHelpers.Txt( "No editor session active." );
			var us = session.UndoSystem;
			if ( us == null ) return OzmiumSceneHelpers.Txt( "UndoSystem not available." );
			us.Undo();
			return OzmiumSceneHelpers.Txt( "Undo performed." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	internal static object Redo()
	{
		try
		{
			var session = SceneEditorSession.Active;
			if ( session == null ) return OzmiumSceneHelpers.Txt( "No editor session active." );
			var us = session.UndoSystem;
			if ( us == null ) return OzmiumSceneHelpers.Txt( "UndoSystem not available." );
			us.Redo();
			return OzmiumSceneHelpers.Txt( "Redo performed." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── set_game_object_transform ──────────────────────────────────────────

	internal static object SetGameObjectTransform( 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
		{
			if ( args.TryGetProperty( "position", out var posEl ) && posEl.ValueKind == JsonValueKind.Object )
			{
				float x = 0, y = 0, z = 0;
				if ( posEl.TryGetProperty( "x", out var xp ) ) x = xp.GetSingle();
				if ( posEl.TryGetProperty( "y", out var yp ) ) y = yp.GetSingle();
				if ( posEl.TryGetProperty( "z", out var zp ) ) z = zp.GetSingle();
				go.WorldPosition = new Vector3( x, y, z );
			}

			if ( args.TryGetProperty( "rotation", out var rotEl ) && rotEl.ValueKind == JsonValueKind.Object )
			{
				float p = 0, yaw = 0, r = 0;
				if ( rotEl.TryGetProperty( "pitch", out var pp ) ) p = pp.GetSingle();
				if ( rotEl.TryGetProperty( "yaw", out var yp ) ) yaw = yp.GetSingle();
				if ( rotEl.TryGetProperty( "roll", out var rp ) ) r = rp.GetSingle();
				go.WorldRotation = Rotation.From( p, yaw, r );
			}

			if ( args.TryGetProperty( "scale", out var scEl ) && scEl.ValueKind == JsonValueKind.Object )
			{
				float x = 1, y = 1, z = 1;
				if ( scEl.TryGetProperty( "x", out var xp ) ) x = xp.GetSingle();
				if ( scEl.TryGetProperty( "y", out var yp ) ) y = yp.GetSingle();
				if ( scEl.TryGetProperty( "z", out var zp ) ) z = zp.GetSingle();
				go.WorldScale = new Vector3( x, y, z );
			}

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message = $"Updated transform for '{go.Name}'.",
				id      = go.Id.ToString(),
				position = OzmiumSceneHelpers.V3( go.WorldPosition ),
				rotation = OzmiumSceneHelpers.Rot( go.WorldRotation ),
				scale    = OzmiumSceneHelpers.V3( go.WorldScale )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── duplicate_game_object ──────────────────────────────────────────────

	internal static object DuplicateGameObject( 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 );
		string newName = OzmiumSceneHelpers.Get( args, "newName", (string)null );

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

		try
		{
			// Clone() creates objects in Game.ActiveScene which may differ from the
			// editor session scene, causing cross-scene parenting issues. Instead,
			// serialize + deserialize into the correct scene to get a full deep copy.
			var json = go.Serialize();
			var clone = scene.CreateObject( false );
			clone.Deserialize( json );

			if ( !string.IsNullOrEmpty( newName ) )
				clone.Name = newName;
			else
			{
				clone.Name = go.Name;
				clone.MakeNameUnique();
			}

			if ( args.TryGetProperty( "position", out var posEl ) && posEl.ValueKind == JsonValueKind.Object )
			{
				float x = 0, y = 0, z = 0;
				if ( posEl.TryGetProperty( "x", out var xp ) ) x = xp.GetSingle();
				if ( posEl.TryGetProperty( "y", out var yp ) ) y = yp.GetSingle();
				if ( posEl.TryGetProperty( "z", out var zp ) ) z = zp.GetSingle();
				clone.WorldPosition = new Vector3( x, y, z );
			}

			clone.Enabled = true;

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				message  = $"Duplicated '{go.Name}' as '{clone.Name}'.",
				id       = clone.Id.ToString(),
				name     = clone.Name,
				position = OzmiumSceneHelpers.V3( clone.WorldPosition )
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── set_game_object_enabled ────────────────────────────────────────────

	internal static object SetGameObjectEnabled( 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 );
		bool?  enabled = args.ValueKind != JsonValueKind.Undefined && args.TryGetProperty( "enabled", out var eEl )
			? eEl.GetBoolean() : (bool?)null;

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

		try
		{
			if ( enabled.HasValue )
				go.Enabled = enabled.Value;
			else
				go.Enabled = !go.Enabled;

			return OzmiumSceneHelpers.Txt( $"'{go.Name}' is now {(go.Enabled ? "enabled" : "disabled")}." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── set_game_object_name ───────────────────────────────────────────────

	internal static object SetGameObjectName( 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 );
		string newName = OzmiumSceneHelpers.Get( args, "newName", (string)null );

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

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

		try
		{
			var oldName = go.Name;
			go.Name = newName;
			return OzmiumSceneHelpers.Txt( $"Renamed '{oldName}' to '{go.Name}' (ID: {go.Id})." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── set_component_enabled ──────────────────────────────────────────────

	internal static object SetComponentEnabled( 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 );
		string compType = OzmiumSceneHelpers.Get( args, "componentType", (string)null );
		bool   enabled  = OzmiumSceneHelpers.Get( args, "enabled",       true );

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

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

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

		try
		{
			comp.Enabled = enabled;
			return OzmiumSceneHelpers.Txt( $"Set '{comp.GetType().Name}' on '{go.Name}' to {(enabled ? "enabled" : "disabled")}." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── Private helpers ─────────────────────────────────────────────────────

	/// <summary>
	/// Fast component type lookup using TypeLibrary (indexed).
	/// Prefers game-assembly types over Sandbox built-ins when names collide.
	/// </summary>
	internal static TypeDescription FindComponentTypeDescription( string typeName )
	{
		// Search all component types in TypeLibrary for a name match.
		// TypeLibrary.GetTypes<Component>() is indexed and fast.
		// We search here FIRST so we can prefer game-assembly types over engine built-ins.
		TypeDescription fallback = null;
		foreach ( var candidate in TypeLibrary.GetTypes<Component>() )
		{
			if ( !candidate.Name.Equals( typeName, StringComparison.OrdinalIgnoreCase ) )
				continue;

			// Prefer game types (not in Sandbox namespace) over engine built-ins
			var ns = candidate.TargetType.Namespace ?? "";
			if ( !ns.StartsWith( "Sandbox", StringComparison.OrdinalIgnoreCase ) )
				return candidate; // Game type found — use it immediately

			fallback ??= candidate; // Remember the first engine match as fallback
		}

		if ( fallback != null ) return fallback;

		// Last resort: try exact match by full type name (for edge cases)
		var td = TypeLibrary.GetType( typeName );
		if ( td != null && td.TargetType.IsClass && !td.TargetType.IsAbstract
			&& typeof( Component ).IsAssignableFrom( td.TargetType ) )
			return td;

		return null;
	}

	private static object ConvertJsonValue( JsonElement el, Type targetType )
	{
		if ( targetType == typeof( string ) )
			return el.ValueKind == JsonValueKind.String ? el.GetString() : el.GetRawText();

		if ( targetType == typeof( bool ) )
		{
			if ( el.ValueKind == JsonValueKind.True )  return true;
			if ( el.ValueKind == JsonValueKind.False ) return false;
			if ( el.ValueKind == JsonValueKind.String ) return bool.Parse( el.GetString() );
			return el.GetBoolean();
		}

		if ( targetType == typeof( int ) )
		{
			if ( el.ValueKind == JsonValueKind.String ) return int.Parse( el.GetString() );
			return el.GetInt32();
		}

		if ( targetType == typeof( float ) )
		{
			if ( el.ValueKind == JsonValueKind.String ) return float.Parse( el.GetString(), System.Globalization.CultureInfo.InvariantCulture );
			return el.GetSingle();
		}

		if ( targetType == typeof( double ) )
		{
			if ( el.ValueKind == JsonValueKind.String ) return double.Parse( el.GetString(), System.Globalization.CultureInfo.InvariantCulture );
			return el.GetDouble();
		}

		if ( targetType == typeof( Vector3 ) && el.ValueKind == JsonValueKind.Object )
		{
			float vx = 0, vy = 0, vz = 0;
			if ( el.TryGetProperty( "x", out var xp ) ) vx = xp.GetSingle();
			if ( el.TryGetProperty( "y", out var yp ) ) vy = yp.GetSingle();
			if ( el.TryGetProperty( "z", out var zp ) ) vz = zp.GetSingle();
			return new Vector3( vx, vy, vz );
		}

		if ( targetType.IsEnum )
		{
			var str = el.ValueKind == JsonValueKind.String ? el.GetString() : el.GetRawText();
			return Enum.Parse( targetType, str, ignoreCase: true );
		}

		// Handle Sandbox.Model (e.g. for SkinnedModelRenderer.Model, ModelRenderer.Model)
		if ( targetType == typeof( Model ) )
		{
			var path = el.ValueKind == JsonValueKind.String ? el.GetString() : el.GetRawText();
			if ( string.IsNullOrEmpty( path ) || path == "null" ) return null;
			return Model.Load( path );
		}

		if ( typeof( Component ).IsAssignableFrom( targetType ) )
		{
			string guidStr = null;
			if ( el.ValueKind == JsonValueKind.String )
				guidStr = el.GetString();
			else if ( el.ValueKind == JsonValueKind.Object )
			{
				if ( el.TryGetProperty( "Id", out var idProp ) ) guidStr = idProp.GetString();
				else if ( el.TryGetProperty( "id", out var idProp2 ) ) guidStr = idProp2.GetString();
			}

			if ( !string.IsNullOrEmpty( guidStr ) && Guid.TryParse( guidStr, out var compGuid ) )
			{
				var scene = OzmiumSceneHelpers.ResolveScene();
				if ( scene != null )
				{
					foreach ( var go in OzmiumSceneHelpers.WalkAll( scene, true ) )
					{
						var match = go.Components.GetAll().FirstOrDefault( c => c.Id == compGuid );
						if ( match != null && targetType.IsAssignableFrom( match.GetType() ) )
							return match;
					}
				}
			}
			return null;
		}

		if ( typeof( GameObject ).IsAssignableFrom( targetType ) )
		{
			string guidStr = null;
			if ( el.ValueKind == JsonValueKind.String )
				guidStr = el.GetString();
			else if ( el.ValueKind == JsonValueKind.Object )
			{
				if ( el.TryGetProperty( "Id", out var idProp ) ) guidStr = idProp.GetString();
				else if ( el.TryGetProperty( "id", out var idProp2 ) ) guidStr = idProp2.GetString();
			}

			if ( !string.IsNullOrEmpty( guidStr ) && Guid.TryParse( guidStr, out var goGuid ) )
			{
				var scene = OzmiumSceneHelpers.ResolveScene();
				if ( scene != null )
				{
					return OzmiumSceneHelpers.WalkAll( scene, true ).FirstOrDefault( g => g.Id == goGuid );
				}
			}
			return null;
		}

		// Fallback: try parsing from string
		var raw = el.ValueKind == JsonValueKind.String ? el.GetString() : el.GetRawText();
		return Convert.ChangeType( raw, targetType );
	}

	// ── Tool schemas (used by ToolDefinitions.All) ───────────────────────────

	internal static Dictionary<string, object> SchemaCreateGameObject => OzmiumSceneHelpers.S( "create_game_object",
		"Create a new empty GameObject in the current scene.",
		OzmiumSceneHelpers.Ps( ("name", "string", "Name (default 'New GameObject')."), ("parentId", "string", "Parent GUID.") ) );

	internal static Dictionary<string, object> SchemaAddComponent => OzmiumSceneHelpers.S( "add_component",
		"Add a component to a GameObject. Use exact C# class name (e.g. 'PointLight', 'ModelRenderer').",
		OzmiumSceneHelpers.Ps( ("id","string","GUID."), ("name","string","Exact name."), ("componentType","string","C# class name.") ),
		new[] { "componentType" } );

	internal static Dictionary<string, object> SchemaRemoveComponent => OzmiumSceneHelpers.S( "remove_component",
		"Remove a component from a GameObject.",
		OzmiumSceneHelpers.Ps( ("id","string","GUID."), ("name","string","Exact name."), ("componentType","string","Type substring.") ),
		new[] { "componentType" } );

	internal static Dictionary<string, object> SchemaSetComponentProperty => OzmiumSceneHelpers.S( "set_component_property",
		"Set a property on a component. Supports string, bool, int, float, Vector3 {x,y,z}, enum.",
		new Dictionary<string, object>
		{
			["id"]            = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "GUID." },
			["name"]          = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Exact name." },
			["componentType"] = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Type substring." },
			["propertyName"]  = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Exact C# property name." },
			["value"]         = new Dictionary<string, object> { ["description"] = "The value to set. Can be string, number, boolean, object {x,y,z} for Vector3, or Component/GameObject GUID." }
		},
		new[] { "propertyName" } );

	internal static Dictionary<string, object> SchemaDestroyGameObject => OzmiumSceneHelpers.S( "destroy_game_object",
		"Delete a GameObject.",
		OzmiumSceneHelpers.Ps( ("id","string","GUID."), ("name","string","Exact name.") ) );

	internal static Dictionary<string, object> SchemaReparentGameObject => OzmiumSceneHelpers.S( "reparent_game_object",
		"Move a GameObject under a new parent. Pass parentId='null' for root.",
		OzmiumSceneHelpers.Ps( ("id","string","GUID."), ("name","string","Exact name."), ("parentId","string","New parent GUID or 'null'.") ) );

	internal static Dictionary<string, object> SchemaSetGameObjectTags => OzmiumSceneHelpers.S( "set_game_object_tags",
		"Set/add/remove tags. Use 'set' array to replace all, 'add'/'remove' for incremental.",
		new Dictionary<string, object>
		{
			["id"]     = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
			["name"]   = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
			["set"]    = new Dictionary<string, object> { ["type"] = "array",  ["description"] = "Replace all tags with this list.", ["items"] = new Dictionary<string, object> { ["type"] = "string" } },
			["add"]    = new Dictionary<string, object> { ["type"] = "array",  ["description"] = "Tags to add.", ["items"] = new Dictionary<string, object> { ["type"] = "string" } },
			["remove"] = new Dictionary<string, object> { ["type"] = "array",  ["description"] = "Tags to remove.", ["items"] = new Dictionary<string, object> { ["type"] = "string" } },
		} );

	internal static Dictionary<string, object> SchemaInstantiatePrefab => OzmiumSceneHelpers.S( "instantiate_prefab",
		"Spawn a prefab at a world position. Use browse_assets to find the path first.",
		OzmiumSceneHelpers.Ps( ("path","string","Prefab asset path."), ("x","number","World X."), ("y","number","World Y."), ("z","number","World Z."), ("parentId","string","Optional parent GUID.") ),
		new[] { "path" } );

	internal static Dictionary<string, object> SchemaSaveScene => OzmiumSceneHelpers.S( "save_scene",
		"Save the currently open scene or prefab to disk.",
		new Dictionary<string, object>() );

	internal static Dictionary<string, object> SchemaUndo => OzmiumSceneHelpers.S( "undo",
		"Undo the last editor operation.",
		new Dictionary<string, object>() );

	internal static Dictionary<string, object> SchemaRedo => OzmiumSceneHelpers.S( "redo",
		"Redo the last undone editor operation.",
		new Dictionary<string, object>() );

	internal static Dictionary<string, object> SchemaSetGameObjectTransform => OzmiumSceneHelpers.S( "set_game_object_transform",
		"Set position, rotation, and scale of a GameObject in one call.",
		new Dictionary<string, object>
		{
			["id"]       = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "GUID." },
			["name"]     = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Exact name." },
			["position"] = new Dictionary<string, object> { ["type"] = "object",  ["description"] = "World position {x,y,z}." },
			["rotation"] = new Dictionary<string, object> { ["type"] = "object",  ["description"] = "World rotation {pitch,yaw,roll} in degrees." },
			["scale"]    = new Dictionary<string, object> { ["type"] = "object",  ["description"] = "World scale {x,y,z}." }
		} );

	internal static Dictionary<string, object> SchemaDuplicateGameObject => OzmiumSceneHelpers.S( "duplicate_game_object",
		"Clone a GameObject with optional new position/name.",
		new Dictionary<string, object>
		{
			["id"]       = new Dictionary<string, object> { ["type"] = "string", ["description"] = "GUID." },
			["name"]     = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Exact name." },
			["position"] = new Dictionary<string, object> { ["type"] = "object", ["description"] = "Optional world position {x,y,z} for the clone." },
			["newName"]  = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Optional new name for the clone." }
		} );

	internal static Dictionary<string, object> SchemaSetGameObjectEnabled => OzmiumSceneHelpers.S( "set_game_object_enabled",
		"Toggle a GameObject's enabled state.",
		new Dictionary<string, object>
		{
			["id"]      = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "GUID." },
			["name"]    = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Exact name." },
			["enabled"] = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Set enabled state. Omit to toggle." }
		} );

	internal static Dictionary<string, object> SchemaSetGameObjectName => OzmiumSceneHelpers.S( "set_game_object_name",
		"Rename a GameObject.",
		OzmiumSceneHelpers.Ps( ("id","string","GUID."), ("name","string","Exact name."), ("newName","string","New name for the object.") ),
		new[] { "newName" } );

	internal static Dictionary<string, object> SchemaSetComponentEnabled => OzmiumSceneHelpers.S( "set_component_enabled",
		"Toggle a component's enabled state.",
		new Dictionary<string, object>
		{
			["id"]            = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "GUID." },
			["name"]          = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Exact name." },
			["componentType"] = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Type substring." },
			["enabled"]       = new Dictionary<string, object> { ["type"] = "boolean", ["description"] = "Enabled state." }
		},
		new[] { "componentType", "enabled" } );
}