Editor/Tools/CodeAwarenessTools.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using Sandbox;

namespace Editor.Mcp
{
	public static partial class SboxMcpAssistant
	{
		[McpTool("pocketknife_find_type")]
		public static string FindType([Description("Type name or fragment")] string name, [Description("Maximum results")] int limit = 20)
		{
			try
			{
				var items = AppDomain.CurrentDomain.GetAssemblies().SelectMany(SafeTypes).Where(t => t.Name.Contains(name ?? "", StringComparison.OrdinalIgnoreCase) || (t.FullName?.Contains(name ?? "", StringComparison.OrdinalIgnoreCase) ?? false)).Take(Math.Clamp(limit, 1, 100)).Select(t => new { FullName = t.FullName, Name = t.Name, Kind = t.IsEnum ? "Enum" : t.IsClass ? "Class" : t.IsValueType ? "Struct" : "Interface", Base = t.BaseType?.FullName }).ToArray();
				return McpJson.Write(ToolOutcome<object>.Ready(new { Query = name, Count = items.Length, Items = items }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "type.search_failed", "Type search failed.", ex)); }
		}

		[McpTool("pocketknife_type_members")]
		public static string TypeMembers([Description("Exact or short type name")] string typeName)
		{
			try
			{
				var type = AppDomain.CurrentDomain.GetAssemblies().SelectMany(SafeTypes).FirstOrDefault(t => t.FullName == typeName || t.Name == typeName); if (type is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Degraded, "type.not_found", $"Type '{typeName}' was not found."));
				var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Take(200).Select(p => new { p.Name, Type = p.PropertyType.FullName, CanRead = p.CanRead, CanWrite = p.CanWrite }); var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).Where(m => !m.IsSpecialName).Take(300).Select(m => new { m.Name, ReturnType = m.ReturnType.FullName, Parameters = m.GetParameters().Select(p => new { p.Name, Type = p.ParameterType.FullName }).ToArray() });
				return McpJson.Write(ToolOutcome<object>.Ready(new { Type = type.FullName, Properties = properties, Methods = methods }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "type.members_failed", "Type member inspection failed.", ex)); }
		}

		[McpTool("pocketknife_compile_await")]
		public static string CompileAwait(string scope = "project", int timeoutMs = 120000)
		{
			var normalizedScope = (scope ?? "").Trim().ToLowerInvariant();
			if (normalizedScope != "project")
				return McpJson.Write(ToolOutcome<object>.Failure(new { Scope = scope }, ToolStatus.Failed, "compile.invalid_scope", "Only project compile scope is supported."));

			var boundedTimeout = Math.Clamp(timeoutMs, 1000, 120000);
			var started = DateTime.UtcNow;
			var baselineState = EditorRuntime.InvokeStatic("Editor.Mcp.EditorTools", "CompileStatus");
			var baselineRevision = CompilerRevision(baselineState);
			CompilerIsSettled(baselineState, out var baselineDiagnostics);
			// Official MCP owns compile dispatch; this method only waits for compiler readback.

			object? observedState = null;
			List<string> observedDiagnostics = new();
			var deadline = started.AddMilliseconds(boundedTimeout);
			var settled = false;
			while (DateTime.UtcNow < deadline)
			{
				observedState = EditorRuntime.InvokeStatic("Editor.Mcp.EditorTools", "CompileStatus");
				settled = CompilerIsSettled(observedState, out observedDiagnostics);
				if (settled) break;
				System.Threading.Thread.Sleep(100);
			}

			var observedRevision = CompilerRevision(observedState);
			var revisionChanged = !string.Equals(baselineRevision, observedRevision, StringComparison.Ordinal);
			var postconditionVerified = settled;
			var value = new
			{
				Scope = normalizedScope,
				TimeoutMs = boundedTimeout,
				BaselineRevision = baselineRevision,
				ObservedRevision = observedRevision,
				RevisionChanged = revisionChanged,
				ElapsedMs = (long)(DateTime.UtcNow - started).TotalMilliseconds,
				Diagnostics = observedDiagnostics.Count > 0 ? observedDiagnostics : baselineDiagnostics,
				PostconditionVerified = postconditionVerified
			};
			return postconditionVerified
				? McpJson.Write(ToolOutcome<object>.Ready(value))
				: McpJson.Write(ToolOutcome<object>.Degraded(value, "compile.await_timeout", "Compiler freshness did not reach a verified settled state before timeout."));
		}

		private static bool CompilerIsSettled(object? state, out List<string> diagnostics)
		{
			diagnostics = new List<string>();
			if (state is null || (EditorRuntime.GetMember(state, "IsBuilding") is bool building && building)) return false;
			var compilers = EditorRuntime.GetMember(state, "Compilers") as System.Collections.IEnumerable;
			if (compilers is null) return false;
			var sawCompiler = false;
			foreach (var compiler in compilers)
			{
				sawCompiler = true;
				if (EditorRuntime.GetMember(compiler, "IsBuilding") is bool compilerBuilding && compilerBuilding) return false;
				if (EditorRuntime.GetMember(compiler, "NeedsBuild") is bool needsBuild && needsBuild) return false;
				if (EditorRuntime.GetMember(compiler, "Success") is bool success && !success) return false;
				if (EditorRuntime.GetMember(compiler, "Errors") is int errors && errors > 0) return false;
				var name = EditorRuntime.GetMember(compiler, "Name")?.ToString() ?? "compiler";
				var warningCount = EditorRuntime.GetMember(compiler, "Warnings")?.ToString() ?? "0";
				diagnostics.Add($"{name}: warnings={warningCount}");
			}
			return sawCompiler;
		}

		private static string CompilerRevision(object? state)
		{
			var assembly = typeof(SboxMcpAssistant).Assembly;
			var location = assembly.Location ?? "";
			var fileStamp = File.Exists(location) ? $"{new FileInfo(location).Length}:{File.GetLastWriteTimeUtc(location).Ticks}" : "memory";
			var parts = new List<string> { location, fileStamp, EditorRuntime.GetMember(state, "IsBuilding")?.ToString() ?? "unknown" };
			if (EditorRuntime.GetMember(state, "Compilers") is System.Collections.IEnumerable compilers)
				foreach (var compiler in compilers) parts.Add(string.Join(":", EditorRuntime.GetMember(compiler, "Name"), EditorRuntime.GetMember(compiler, "NeedsBuild"), EditorRuntime.GetMember(compiler, "Success"), EditorRuntime.GetMember(compiler, "Errors")));
			return string.Join("|", parts);
		}

		[McpTool("pocketknife_package_references")]
		public static string PackageReferences()
		{
			try
			{
				var root = Project.Current?.GetRootPath(); if (string.IsNullOrWhiteSpace(root)) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "project.not_active", "No active project is available.")); var sbproj = Directory.EnumerateFiles(root, "*.sbproj", SearchOption.TopDirectoryOnly).FirstOrDefault(); if (sbproj is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "project.sbproj_missing", "No .sbproj was found in the active project root."));
				using var doc = JsonDocument.Parse(File.ReadAllText(sbproj)); var refs = doc.RootElement.TryGetProperty("PackageReferences", out var p) && p.ValueKind == JsonValueKind.Array ? p.EnumerateArray().Select(x => x.GetString() ?? x.ToString()).ToArray() : Array.Empty<string>(); return McpJson.Write(ToolOutcome<object>.Ready(new { ProjectFile = sbproj, PackageReferences = refs, Count = refs.Length }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "project.references_failed", "Package reference parsing failed.", ex)); }
		}

		private static IEnumerable<Type> SafeTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch { return Array.Empty<Type>(); } }
	}
}