Editor/Mcp/Docs/ApiSearch.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SboxMcp.Mcp.Docs;

public sealed class ApiSearchResult
{
	public string FullName { get; set; }
	public string Name { get; set; }
	public string Namespace { get; set; }
	public string Description { get; set; }
	public string Url { get; set; }
	public List<string> TopMembers { get; set; } = new();
	public double Score { get; set; }
}

public sealed class ApiSearch
{
	private FuzzyIndex<ApiType> _index = CreateIndex();
	private readonly Dictionary<string, ApiType> _byName = new( StringComparer.OrdinalIgnoreCase );

	private static FuzzyIndex<ApiType> CreateIndex() => new(
		new FuzzyIndexConfig
		{
			Fields = new[]
			{
				new IndexedField( "name", 4.0 ),
				new IndexedField( "fullName", 3.0 ),
				new IndexedField( "memberNames", 2.0 ),
				new IndexedField( "namespace", 1.5 ),
				new IndexedField( "description", 1.0 ),
			},
		},
		type => new Dictionary<string, string>
		{
			["name"] = type.Name,
			["fullName"] = type.FullName,
			["namespace"] = type.Namespace ?? "",
			["description"] = type.Documentation?.Summary ?? "",
			["memberNames"] = string.Join( ' ', CollectMemberNames( type ) ),
		} );

	public int TypeCount
	{
		get
		{
			var seen = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
			foreach ( var t in _byName.Values ) seen.Add( t.FullName );
			return seen.Count;
		}
	}

	public void BuildIndex( IEnumerable<ApiType> types )
	{
		_index = CreateIndex();
		_byName.Clear();
		foreach ( var t in types )
		{
			_index.Add( t );
			_byName[t.FullName] = t;
			if ( !_byName.ContainsKey( t.Name ) )
				_byName[t.Name] = t;
		}
	}

	public IReadOnlyList<ApiSearchResult> Search( string query, int limit = 8 )
	{
		return _index.Search( query, limit )
			.Select( h => new ApiSearchResult
			{
				FullName = h.Item.FullName,
				Name = h.Item.Name,
				Namespace = h.Item.Namespace ?? "",
				Description = h.Item.Documentation?.Summary ?? "",
				Url = TypeUrl( h.Item ),
				TopMembers = CollectMemberNames( h.Item ).Take( 5 ).ToList(),
				Score = h.Score,
			} )
			.ToList();
	}

	public ApiType LookupType( string name ) =>
		_byName.TryGetValue( name, out var t ) ? t : null;

	internal static string TypeUrl( ApiType t ) => $"https://sbox.game/api/t/{t.FullName}";

	internal static IEnumerable<string> CollectMemberNames( ApiType t )
	{
		if ( t.Methods is { Count: > 0 } )
			foreach ( var m in t.Methods ) yield return m.Name;
		if ( t.Properties is { Count: > 0 } )
			foreach ( var p in t.Properties ) yield return p.Name;
		if ( t.Fields is { Count: > 0 } )
			foreach ( var f in t.Fields ) yield return f.Name;
	}

	public static string FormatTypeDetail( ApiType type, int startIndex, int maxLength )
	{
		var sb = new StringBuilder();
		var kind = type.IsInterface ? "interface" : type.IsAbstract ? "abstract class" : "class";
		sb.AppendLine( $"# {type.FullName}" );
		sb.AppendLine( $"**Type:** {kind} | **Namespace:** {(string.IsNullOrEmpty( type.Namespace ) ? "(global)" : type.Namespace)}" );
		if ( !string.IsNullOrEmpty( type.BaseType ) )
			sb.AppendLine( $"**Inherits:** {type.BaseType}" );
		var url = TypeUrl( type );
		sb.AppendLine( $"**URL:** [{url}]({url})" );
		sb.AppendLine();

		if ( !string.IsNullOrEmpty( type.Documentation?.Summary ) )
		{
			sb.AppendLine( type.Documentation.Summary );
			sb.AppendLine();
		}

		if ( type.Constructors is { Count: > 0 } )
		{
			sb.AppendLine( "## Constructors" );
			foreach ( var c in type.Constructors )
			{
				sb.AppendLine( $"- `{FormatMethodSignature( c )}`" );
				if ( !string.IsNullOrEmpty( c.Documentation?.Summary ) )
					sb.AppendLine( $"  {c.Documentation.Summary}" );
			}
			sb.AppendLine();
		}

		if ( type.Properties is { Count: > 0 } )
		{
			sb.AppendLine( "## Properties" );
			foreach ( var p in type.Properties )
			{
				var stat = p.IsStatic ? "static " : "";
				sb.AppendLine( $"- `{stat}{p.PropertyType ?? "?"} {p.Name}`" );
				if ( !string.IsNullOrEmpty( p.Documentation?.Summary ) )
					sb.AppendLine( $"  {p.Documentation.Summary}" );
			}
			sb.AppendLine();
		}

		if ( type.Methods is { Count: > 0 } )
		{
			sb.AppendLine( "## Methods" );
			foreach ( var m in type.Methods )
			{
				var stat = m.IsStatic ? "static " : "";
				sb.AppendLine( $"- `{stat}{FormatMethodSignature( m )}`" );
				if ( !string.IsNullOrEmpty( m.Documentation?.Summary ) )
					sb.AppendLine( $"  {m.Documentation.Summary}" );
			}
			sb.AppendLine();
		}

		if ( type.Fields is { Count: > 0 } )
		{
			sb.AppendLine( "## Fields" );
			foreach ( var f in type.Fields )
			{
				var stat = f.IsStatic ? "static " : "";
				sb.AppendLine( $"- `{stat}{f.FieldType ?? "?"} {f.Name}`" );
				if ( !string.IsNullOrEmpty( f.Documentation?.Summary ) )
					sb.AppendLine( $"  {f.Documentation.Summary}" );
			}
			sb.AppendLine();
		}

		var full = sb.ToString();
		var totalLength = full.Length;
		var start = Math.Min( startIndex, totalLength );
		var clampedLength = Math.Min( maxLength, totalLength - start );
		var chunk = full.Substring( start, clampedLength );
		var endIndex = start + chunk.Length;
		var hasMore = endIndex < totalLength;

		var footer = hasMore
			? $"\n\n---\n_Showing characters {start}–{endIndex} of {totalLength}. Use start_index={endIndex} to read the next chunk._"
			: $"\n\n---\n_End of type page ({totalLength} characters total)._";

		return chunk + footer;
	}

	private static string FormatMethodSignature( ApiMethod m )
	{
		var paramStr = "";
		if ( m.Parameters is { Count: > 0 } )
		{
			paramStr = string.Join( ", ", m.Parameters.Select( p =>
			{
				var prefix = p.Out ? "out " : "";
				return string.IsNullOrEmpty( p.Type ) ? $"{prefix}{p.Name}" : $"{prefix}{p.Type} {p.Name}";
			} ) );
		}
		var ret = string.IsNullOrEmpty( m.ReturnType ) || m.ReturnType == "void" ? "void" : m.ReturnType;
		return $"{ret} {m.Name}({paramStr})";
	}
}