Editor/Tools/GraphTools.cs
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using Editor;
using Sandbox;
using SboxMcp.Registry;
using static SboxMcp.Tools.AssetTools;
using static SboxMcp.Tools.ToolHelpers;

namespace SboxMcp.Tools;

/// <summary>
/// AnimGraph (.vanmgrph, KV3), ShaderGraph (.shdrgrph, JSON) and
/// ActionGraph (JSON) authoring at the file-format level, with compile
/// verification through the asset system.
/// </summary>
public static class GraphTools
{
	// ---- AnimGraph -------------------------------------------------------

	internal const string AnimGraphHeader =
		"<!-- kv3 encoding:text:version{e21c7f3c-8a33-41c5-9977-a76d3a32aa0d} format:animgraph2:version{0f7898b8-5471-45c4-9867-cd9c46bcfdb5} -->";

	[McpTool( "animgraph_get", "Reads an animation graph (.vanmgrph) as JSON (or raw KV3). Shows nodes, states, transitions and parameters.", ToolCategory.AnimGraph )]
	public static object AnimGraphGet(
		[Desc( "Animgraph asset path, e.g. 'models/citizen/citizen.vanmgrph'" )] string path,
		[Desc( "Return raw KV3 text instead of a JSON view" )] bool raw = false )
	{
		var asset = AssetSystem.FindByPath( path )
			?? throw new InvalidOperationException( $"No animgraph at '{path}' - use asset_search with assetType 'vanmgrph'" );

		var text = File.ReadAllText( RequireSourceFile( asset ) );

		if ( raw )
			return new { path = asset.Path, format = "kv3", content = text };

		var json = EditorUtility.KeyValues3ToJson( text );
		if ( json is null )
			return new { path = asset.Path, format = "kv3 (JSON conversion unavailable for this graph; this is the raw source)", content = text };

		return new { path = asset.Path, format = "json (read-only view; write with animgraph_set using KV3)", content = json };
	}

	[McpTool( "animgraph_set", "Writes full KV3 content to a .vanmgrph (creating it if missing), then compiles. Read an existing graph with animgraph_get raw=true to learn the format.", ToolCategory.AnimGraph, Writes = true )]
	public static object AnimGraphSet(
		[Desc( "Animgraph asset path or project-relative path ending in .vanmgrph" )] string path,
		[Desc( "Complete KV3 file content. The '<!-- kv3 ... -->' header is added automatically when missing." )] string kv3Content )
	{
		if ( !kv3Content.TrimStart().StartsWith( "<!--" ) )
			kv3Content = AnimGraphHeader + "\n" + kv3Content;

		return AssetTools.WriteRaw( path, kv3Content );
	}

	[McpTool( "animgraph_list_parameters", "Lists an animation graph's parameters with their value types (the knobs SkinnedModelRenderer.Set drives).", ToolCategory.AnimGraph )]
	public static object AnimGraphListParameters( [Desc( "Animgraph asset path" )] string path )
	{
		var asset = AssetSystem.FindByPath( path )
			?? throw new InvalidOperationException( $"No animgraph at '{path}' - use asset_search with assetType 'vanmgrph'" );

		var graph = AnimationGraph.Load( asset.Path )
			?? throw new InvalidOperationException( $"'{path}' failed to load - is it compiled? Try asset_compile first" );

		var parameters = Enumerable.Range( 0, graph.ParamCount )
			.Select( i => new
			{
				name = graph.GetParameterName( i ),
				type = graph.GetParameterType( i )?.Name
			} )
			.ToArray();

		return new { path = asset.Path, count = parameters.Length, parameters };
	}

	[McpTool( "animator_list_parameters", "Lists every parameter on a GameObject's active SkinnedModelRenderer animation graph, including each current value. Use this before wiring animation code so parameter names and types are never guessed.", ToolCategory.AnimGraph )]
	public static object AnimatorListParameters(
		[Desc( "GameObject id or unique name with a SkinnedModelRenderer" )] string gameObject )
	{
		var go = FindGameObject( gameObject );
		var renderer = go.Components.Get<SkinnedModelRenderer>()
			?? throw new InvalidOperationException( $"'{go.Name}' has no SkinnedModelRenderer" );
		var graph = renderer.Parameters.Graph ?? renderer.AnimationGraph
			?? throw new InvalidOperationException( $"'{go.Name}' has no active animation graph" );

		var parameters = Enumerable.Range( 0, graph.ParamCount )
			.Select( i =>
			{
				var name = graph.GetParameterName( i );
				var type = graph.GetParameterType( i );
				return new { name, type = type?.Name, value = AnimatorValue( renderer, name, type ) };
			} )
			.ToArray();

		return new { gameObjectId = go.Id, graph = graph.Name, count = parameters.Length, parameters };
	}

	static object AnimatorValue( SkinnedModelRenderer renderer, string name, Type type ) => type switch
	{
		_ when type == typeof( bool ) => renderer.GetBool( name ),
		_ when type == typeof( float ) => renderer.GetFloat( name ),
		_ when type == typeof( int ) => renderer.GetInt( name ),
		_ when type == typeof( Vector3 ) => V( renderer.GetVector( name ) ),
		_ when type == typeof( Rotation ) => A( renderer.GetRotation( name ) ),
		_ => null
	};

	// ---- ShaderGraph -----------------------------------------------------

	[McpTool( "shadergraph_get", "Reads a shader graph (.shdrgrph). The format is JSON: nodes keyed by id with _class, inputs referencing other nodes' outputs.", ToolCategory.ShaderGraph )]
	public static object ShaderGraphGet( [Desc( "Shadergraph asset path" )] string path )
	{
		var asset = AssetSystem.FindByPath( path )
			?? throw new InvalidOperationException( $"No shadergraph at '{path}' - use asset_search with assetType 'shdrgrph'" );

		return new { path = asset.Path, content = File.ReadAllText( RequireSourceFile( asset ) ) };
	}

	[McpTool( "shadergraph_set", "Writes full JSON content to a .shdrgrph (creating it if missing), then compiles. Read an existing graph first to learn the node schema.", ToolCategory.ShaderGraph, Writes = true )]
	public static object ShaderGraphSet(
		[Desc( "Shadergraph asset path or project-relative path ending in .shdrgrph" )] string path,
		[Desc( "Complete JSON file content" )] string jsonContent )
	{
		ValidateJson( jsonContent, path );
		return AssetTools.WriteRaw( path, jsonContent );
	}

	[McpTool( "shadergraph_list_nodes", "Lists the nodes in a shader graph: id, class and position.", ToolCategory.ShaderGraph )]
	public static object ShaderGraphListNodes( [Desc( "Shadergraph asset path" )] string path )
	{
		var asset = AssetSystem.FindByPath( path )
			?? throw new InvalidOperationException( $"No shadergraph at '{path}' - use asset_search with assetType 'shdrgrph'" );

		var doc = JsonDocument.Parse( File.ReadAllText( RequireSourceFile( asset ) ) );

		if ( !doc.RootElement.TryGetProperty( "nodes", out var nodes ) )
			return new { path = asset.Path, nodes = Array.Empty<object>() };

		var list = nodes.ValueKind switch
		{
			JsonValueKind.Array => nodes.EnumerateArray().Select( DescribeNode ).ToArray(),
			JsonValueKind.Object => nodes.EnumerateObject().Select( p => DescribeNode( p.Value ) ).ToArray(),
			_ => Array.Empty<object>()
		};

		return new { path = asset.Path, count = list.Length, nodes = list };
	}

	static object DescribeNode( JsonElement node ) => new
	{
		id = TryString( node, "Identifier" ) ?? TryString( node, "identifier" ),
		cls = TryString( node, "_class" ),
		position = TryString( node, "Position" ) ?? TryString( node, "position" )
	};

	static string TryString( JsonElement el, string name ) =>
		el.ValueKind == JsonValueKind.Object && el.TryGetProperty( name, out var v ) ? v.ToString() : null;

	// ---- ActionGraph -----------------------------------------------------

	[McpTool( "actiongraph_get", "Reads an action graph asset (visual scripting). The format is JSON.", ToolCategory.ActionGraph )]
	public static object ActionGraphGet( [Desc( "Action graph asset path, e.g. 'graphs/thing.action'" )] string path )
	{
		var asset = AssetSystem.FindByPath( path )
			?? throw new InvalidOperationException( $"No action graph at '{path}' - use asset_search with assetType 'action'" );

		return new { path = asset.Path, content = File.ReadAllText( RequireSourceFile( asset ) ) };
	}

	[McpTool( "actiongraph_set", "Writes full JSON content to an action graph asset (creating it if missing), then compiles. Read an existing graph first to learn the node schema.", ToolCategory.ActionGraph, Writes = true )]
	public static object ActionGraphSet(
		[Desc( "Action graph asset path or project-relative path" )] string path,
		[Desc( "Complete JSON file content" )] string jsonContent )
	{
		ValidateJson( jsonContent, path );
		return AssetTools.WriteRaw( path, jsonContent );
	}

	static void ValidateJson( string json, string path )
	{
		try
		{
			using var _ = JsonDocument.Parse( json );
		}
		catch ( JsonException e )
		{
			throw new ArgumentException( $"Content for '{path}' is not valid JSON: {e.Message}" );
		}
	}
}