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

namespace SboxMcpServer;

/// <summary>
/// Handlers for asset-query and editor-context MCP tools:
/// browse_assets, get_editor_context, get_model_info, get_material_properties,
/// get_prefab_structure, reload_asset.
/// </summary>
internal static class OzmiumAssetHandlers
{
	private static readonly JsonSerializerOptions _json = new()
	{
		PropertyNamingPolicy   = JsonNamingPolicy.CamelCase,
		DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
	};

	// ── browse_assets ──────────────────────────────────────────────────────

	internal static object BrowseAssets( JsonElement args )
	{
		string typeFilter = OzmiumSceneHelpers.Get( args, "type",        (string)null );
		string nameFilter = OzmiumSceneHelpers.Get( args, "nameContains",(string)null );
		int    max        = OzmiumSceneHelpers.Get( args, "maxResults",  100 );

		var results  = new List<Dictionary<string, object>>();
		int total    = 0;

		try
		{
			foreach ( var asset in AssetSystem.All )
			{
				total++;
				var ext  = asset.AssetType?.FileExtension ?? "";
				var aName = asset.Name ?? "";
				var friendly = asset.AssetType?.FriendlyName ?? ext;

				if ( !string.IsNullOrEmpty( typeFilter ) )
				{
					bool match = ext.IndexOf( typeFilter, StringComparison.OrdinalIgnoreCase ) >= 0
					          || friendly.IndexOf( typeFilter, StringComparison.OrdinalIgnoreCase ) >= 0;
					if ( !match ) continue;
				}
				if ( !string.IsNullOrEmpty( nameFilter ) &&
					aName.IndexOf( nameFilter, StringComparison.OrdinalIgnoreCase ) < 0 ) continue;
				if ( results.Count >= max ) break;

				results.Add( new Dictionary<string, object>
				{
					["path"]         = asset.Path ?? "",
					["relativePath"] = asset.RelativePath ?? asset.Path ?? "",
					["name"]         = aName,
					["type"]         = friendly,
					["extension"]    = ext
				} );
			}
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }

		var summary = $"Found {results.Count} asset(s)" +
			( !string.IsNullOrEmpty( typeFilter ) ? $" type='{typeFilter}'" : "" ) +
			( !string.IsNullOrEmpty( nameFilter ) ? $" name='{nameFilter}'" : "" ) +
			$" (scanned {total}).";

		return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new { summary, results }, _json ) );
	}

	// ── get_editor_context ─────────────────────────────────────────────────

	internal static object GetEditorContext()
	{
		var ctx = new Dictionary<string, object>
		{
			["activeGameScene"] = Game.ActiveScene?.Name,
			["isPlaying"]       = Game.ActiveScene != null
		};

		try
		{
			var sessions = new List<Dictionary<string, object>>();
			foreach ( var s in SceneEditorSession.All )
			{
				if ( s == null ) continue;
				sessions.Add( new Dictionary<string, object>
				{
					["isActive"]    = s == SceneEditorSession.Active,
					["sceneName"]   = s.Scene?.Name,
					["objectCount"] = s.Scene != null ? OzmiumSceneHelpers.WalkAll( s.Scene, true ).Count() : 0
				} );
			}
			ctx["editorSessions"]     = sessions;
			ctx["activeSessionScene"] = SceneEditorSession.Active?.Scene?.Name;

			// Current selection
			var sel = new List<Dictionary<string, object>>();
			foreach ( var go in OzmiumSceneHelpers.GetSelectedGameObjects() )
				sel.Add( new Dictionary<string, object>
				{
					["id"] = go.Id.ToString(), ["name"] = go.Name,
					["path"] = OzmiumSceneHelpers.GetObjectPath( go )
				} );
			ctx["selectedObjects"] = sel;
		}
		catch ( Exception ex ) { ctx["editorApiError"] = ex.Message; }

		return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( ctx, _json ) );
	}

	// ── get_model_info ─────────────────────────────────────────────────────

	internal static object GetModelInfo( JsonElement args )
	{
		string path = OzmiumSceneHelpers.NormalizePath( OzmiumSceneHelpers.Get( args, "path", (string)null ) );
		if ( string.IsNullOrEmpty( path ) ) return OzmiumSceneHelpers.Txt( "Provide 'path' (model asset path, e.g. 'models/citizen_male.vmdl')." );

		try
		{
			var model = Model.Load( path );
			if ( model == null ) return OzmiumSceneHelpers.Txt( $"Model not found: '{path}'." );

			var bones = new List<Dictionary<string, object>>();
			// BoneCollection.GetBone(string) takes a name, not an index.
			// We don't have a way to enumerate bone names, so just report the count.
			bones.Add( new Dictionary<string, object>
			{
				["note"] = $"{model.BoneCount} bones total. Use model viewer or VMDL source for bone names."
			} );

			var attachments = new List<Dictionary<string, object>>();
			try
			{
				// Use reflection: ModelAttachments API varies — don't assume Count or indexer exist
				var attObj = model.Attachments;
				if ( attObj != null )
				{
					var countProp = attObj.GetType().GetProperty( "Count" )
					             ?? attObj.GetType().GetProperty( "Length" );
					int count = countProp != null ? (int)countProp.GetValue( attObj ) : 0;
					var indexer = attObj.GetType().GetProperty( "Item" );
					for ( int i = 0; i < count; i++ )
					{
						try
						{
							var att = indexer?.GetValue( attObj, new object[] { i } );
							var attName = att?.GetType().GetProperty( "Name" )?.GetValue( att )?.ToString() ?? $"att_{i}";
							attachments.Add( new Dictionary<string, object> { ["name"] = attName, ["index"] = i } );
						}
						catch { attachments.Add( new Dictionary<string, object> { ["index"] = i } ); }
					}
				}
			}
			catch { attachments.Add( new Dictionary<string, object> { ["name"] = "(attachment iteration not supported)" } ); }

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				path,
				boneCount       = model.BoneCount,
				bones,
				attachmentCount = attachments.Count,
				attachments
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error loading model: {ex.Message}" ); }
	}

	// ── get_material_properties ────────────────────────────────────────────

	internal static object GetMaterialProperties( JsonElement args )
	{
		string path = OzmiumSceneHelpers.NormalizePath( OzmiumSceneHelpers.Get( args, "path", (string)null ) );
		if ( string.IsNullOrEmpty( path ) ) return OzmiumSceneHelpers.Txt( "Provide 'path' (material asset path, e.g. 'materials/dev/dev_01.vmat')." );

		try
		{
			var mat = Material.Load( path );
			if ( mat == null ) return OzmiumSceneHelpers.Txt( $"Material not found: '{path}'." );

			return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
			{
				path,
				name   = mat.Name,
				shader = mat.ShaderName
			}, _json ) );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── get_prefab_structure ────────────────────────────────────────────────

	internal static object GetPrefabStructure( JsonElement args )
	{
		string path = OzmiumSceneHelpers.NormalizePath( OzmiumSceneHelpers.Get( args, "path", (string)null ) );
		if ( string.IsNullOrEmpty( path ) ) return OzmiumSceneHelpers.Txt( "Provide 'path' (relative prefab path, e.g. 'prefabs/player.prefab')." );

		try
		{
			// PrefabFile does not expose a live Scene property when not open in the editor;
			// fall back to reading the raw prefab JSON from disk.
			var asset = AssetSystem.FindByPath( path );
			if ( asset != null && System.IO.File.Exists( asset.AbsolutePath ) )
			{
				var raw = System.IO.File.ReadAllText( asset.AbsolutePath );
				return OzmiumSceneHelpers.Txt( $"Raw prefab JSON for '{path}':\n{raw}" );
			}

			return OzmiumSceneHelpers.Txt( $"Prefab not found: '{path}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── reload_asset ───────────────────────────────────────────────────────

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

		try
		{
			var asset = AssetSystem.FindByPath( path );
			if ( asset == null ) return OzmiumSceneHelpers.Txt( $"Asset not found: '{path}'." );
			asset.Compile( true );
			return OzmiumSceneHelpers.Txt( $"Reimport triggered for '{path}'." );
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }
	}

	// ── get_component_types ────────────────────────────────────────────────

	internal static object GetComponentTypes( JsonElement args )
	{
		string filter = OzmiumSceneHelpers.Get( args, "filter", (string)null );

		var results = new List<Dictionary<string, object>>();
		try
		{
			foreach ( var td in TypeLibrary.GetTypes<Component>() )
			{
				var name = td.Name;
				if ( !string.IsNullOrEmpty( filter )
					&& name.IndexOf( filter, StringComparison.OrdinalIgnoreCase ) < 0
					&& ( td.TargetType?.Namespace ?? "" ).IndexOf( filter, StringComparison.OrdinalIgnoreCase ) < 0 )
					continue;

				// Skip abstract types
				if ( td.TargetType != null && td.TargetType.IsAbstract ) continue;

				results.Add( new Dictionary<string, object>
				{
					["name"]  = name,
					["namespace"] = td.TargetType?.Namespace ?? ""
				} );
			}
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }

		results = results.OrderBy( r => r["name"]?.ToString() ).ToList();
		return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
		{
			summary = $"Found {results.Count} component type(s)" +
				( !string.IsNullOrEmpty( filter ) ? $" matching '{filter}'" : "" ) + ".",
			results
		}, _json ) );
	}

	// ── search_assets ─────────────────────────────────────────────────────

	internal static object SearchAssets( JsonElement args )
	{
		string query   = OzmiumSceneHelpers.Get( args, "query",     (string)null );
		string type    = OzmiumSceneHelpers.Get( args, "type",      (string)null );
		int    max     = OzmiumSceneHelpers.Get( args, "maxResults", 50 );

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

		var results = new List<Dictionary<string, object>>();
		try
		{
			foreach ( var asset in AssetSystem.All )
			{
				if ( results.Count >= max ) break;
				var ext     = asset.AssetType?.FileExtension ?? "";
				var aName   = asset.Name ?? "";
				var aPath   = asset.Path ?? "";
				var friendly = asset.AssetType?.FriendlyName ?? ext;

				// Match query against name and path
				bool match = aName.IndexOf( query, StringComparison.OrdinalIgnoreCase ) >= 0
				          || aPath.IndexOf( query, StringComparison.OrdinalIgnoreCase ) >= 0;
				if ( !match ) continue;

				if ( !string.IsNullOrEmpty( type ) )
				{
					bool typeMatch = ext.IndexOf( type, StringComparison.OrdinalIgnoreCase ) >= 0
					                || friendly.IndexOf( type, StringComparison.OrdinalIgnoreCase ) >= 0;
					if ( !typeMatch ) continue;
				}

				results.Add( new Dictionary<string, object>
				{
					["path"]         = aPath,
					["relativePath"] = asset.RelativePath ?? aPath,
					["name"]         = aName,
					["type"]         = friendly,
					["extension"]    = ext
				} );
			}
		}
		catch ( Exception ex ) { return OzmiumSceneHelpers.Txt( $"Error: {ex.Message}" ); }

		return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( new
		{
			summary = $"Found {results.Count} asset(s) matching '{query}'" +
				( !string.IsNullOrEmpty( type ) ? $" type='{type}'" : "" ) + ".",
			results
		}, _json ) );
	}

	// ── get_scene_statistics ──────────────────────────────────────────────

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

		var allObjects = OzmiumSceneHelpers.WalkAll( scene, true ).ToList();

		// Component type frequency
		var compCounts = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects )
			foreach ( var comp in go.Components.GetAll() )
			{
				var typeName = comp.GetType().Name;
				compCounts.TryGetValue( typeName, out var existing );
				compCounts[typeName] = existing + 1;
			}

		// All unique tags
		var allTags = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects )
			foreach ( var tag in go.Tags.TryGetAll() )
				allTags.Add( tag );

		// Prefab source breakdown
		var prefabCounts = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects.Where( g => g.IsPrefabInstance && g.PrefabInstanceSource != null ) )
		{
			var src = go.PrefabInstanceSource;
			prefabCounts.TryGetValue( src, out var existing );
			prefabCounts[src] = existing + 1;
		}

		// Network mode distribution
		var netModeCounts = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects )
		{
			var mode = go.NetworkMode.ToString();
			netModeCounts.TryGetValue( mode, out var existing );
			netModeCounts[mode] = existing + 1;
		}

		var stats = new Dictionary<string, object>
		{
			["sceneName"]            = scene.Name,
			["totalObjects"]         = allObjects.Count,
			["rootObjects"]          = scene.Children.Count,
			["enabledObjects"]       = allObjects.Count( g => g.Enabled ),
			["disabledObjects"]      = allObjects.Count( g => !g.Enabled ),
			["uniqueTags"]           = allTags.OrderBy( t => t ).ToList(),
			["componentBreakdown"]   = compCounts
				.OrderByDescending( kv => kv.Value )
				.Select( kv => new Dictionary<string, object> { ["type"] = kv.Key, ["count"] = kv.Value } )
				.ToList(),
			["prefabBreakdown"]      = prefabCounts
				.OrderByDescending( kv => kv.Value )
				.Select( kv => new Dictionary<string, object> { ["prefab"] = kv.Key, ["instances"] = kv.Value } )
				.ToList(),
			["networkModeBreakdown"] = netModeCounts
		};

		return OzmiumSceneHelpers.Txt( JsonSerializer.Serialize( stats, _json ) );
	}

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

	private static Dictionary<string, object> P1( string key, string type, string desc )
		=> new Dictionary<string, object> { [key] = new Dictionary<string, object> { ["type"] = type, ["description"] = desc } };

	internal static Dictionary<string, object> SchemaGetModelInfo => OzmiumSceneHelpers.S( "get_model_info",
		"Return bone names, attachment points, and sequence count for a .vmdl model.",
		P1( "path", "string", "Model path (e.g. 'models/citizen_male.vmdl')." ),
		new[] { "path" } );

	internal static Dictionary<string, object> SchemaGetMaterialProperties => OzmiumSceneHelpers.S( "get_material_properties",
		"Return shader name and surface properties for a .vmat material.",
		P1( "path", "string", "Material path (e.g. 'materials/dev/dev_01.vmat')." ),
		new[] { "path" } );

	internal static Dictionary<string, object> SchemaGetPrefabStructure => OzmiumSceneHelpers.S( "get_prefab_structure",
		"Return the full object/component hierarchy of a .prefab file without opening it.",
		P1( "path", "string", "Prefab path (e.g. 'prefabs/player.prefab')." ),
		new[] { "path" } );

	internal static Dictionary<string, object> SchemaReloadAsset => OzmiumSceneHelpers.S( "reload_asset",
		"Force reimport/recompile of a specific asset — useful after modifying source files on disk.",
		P1( "path", "string", "Asset path to reimport." ),
		new[] { "path" } );

	internal static Dictionary<string, object> SchemaGetComponentTypes => OzmiumSceneHelpers.S( "get_component_types",
		"List all available component types via TypeLibrary, so AI knows what components can be added.",
		new Dictionary<string, object>
		{
			["filter"] = new Dictionary<string, object> { ["type"] = "string", ["description"] = "Optional filter string to match against type name or namespace." }
		} );

	internal static Dictionary<string, object> SchemaSearchAssets => OzmiumSceneHelpers.S( "search_assets",
		"Search assets by content (file extension filter + substring matching on name and path).",
		new Dictionary<string, object>
		{
			["query"]      = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Search query (matches name and path)." },
			["type"]       = new Dictionary<string, object> { ["type"] = "string",  ["description"] = "Optional file type filter (e.g. 'prefab', 'vmdl', 'vmat')." },
			["maxResults"] = new Dictionary<string, object> { ["type"] = "integer", ["description"] = "Max results (default 50)." }
		},
		new[] { "query" } );

	internal static Dictionary<string, object> SchemaGetSceneStatistics => OzmiumSceneHelpers.S( "get_scene_statistics",
		"Enhanced scene summary with component type frequency, prefab breakdown, network mode distribution, and tags.",
		new Dictionary<string, object>() );
}