Editor/Tools/PocketknifeComplementTools.cs
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Sandbox;

namespace Editor.Mcp
{
	public static partial class SboxMcpAssistant
	{
		[McpTool("pocketknife_scene_sessions")]
		public static string SceneSessions(int limit = 50, int offset = 0)
		{
			try
			{
				var boundedLimit = Math.Clamp(limit, 1, 100);
				var boundedOffset = Math.Max(offset, 0);
				var snapshots = GetSceneSnapshots().ToArray();
				var items = snapshots.Skip(boundedOffset).Take(boundedLimit).Select(snapshot => new
				{
					SessionId = snapshot.Id,
					Path = snapshot.Path,
					Name = snapshot.Name,
					Active = snapshot.Active,
					Dirty = snapshot.Dirty,
					Mounted = snapshot.Mounted,
					Playing = snapshot.Playing
				}).ToArray();
				return McpJson.Write(ToolOutcome<object>.Ready(new { Count = items.Length, Total = snapshots.Length, Offset = boundedOffset, Limit = boundedLimit, HasMore = boundedOffset + items.Length < snapshots.Length, Sessions = items }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "scene.sessions_failed", "Scene session discovery failed.", ex)); }
		}

		[McpTool("pocketknife_scene_activate")]
		public static string ActivateSceneSession(string sessionIdOrPath)
		{
			try
			{
				if (string.IsNullOrWhiteSpace(sessionIdOrPath) || sessionIdOrPath.Trim().Length > 400)
					return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "scene.session_id_invalid", "A scene session ID or path of at most 400 characters is required."));
				var snapshots = GetSceneSnapshots();
				var requested = sessionIdOrPath.Trim();
				var target = snapshots.FirstOrDefault(snapshot => snapshot.Id.Equals(requested, StringComparison.OrdinalIgnoreCase) || (!string.IsNullOrWhiteSpace(snapshot.Path) && snapshot.Path.Equals(ToolGuard.Normalize(requested), StringComparison.OrdinalIgnoreCase)) || snapshot.Name.Equals(requested, StringComparison.OrdinalIgnoreCase));
				if (target is null)
					return McpJson.Write(ToolOutcome<object>.Failure(new { Requested = requested }, ToolStatus.Unavailable, "scene.session_not_found", "No open scene session matched the requested stable ID or path."));

				EditorRuntime.InvokeInstance(target.Session, "MakeActive", true);
				EditorRuntime.InvokeInstance(target.Session, "BringToFront");
				var deadline = DateTime.UtcNow.AddSeconds(5);
				object? active = null;
				while (DateTime.UtcNow < deadline)
				{
					active = EditorRuntime.GetStaticProperty("Editor.SceneEditorSession", "Active");
					if (ReferenceEquals(active, target.Session)) break;
					Thread.Sleep(50);
				}
				var verified = ReferenceEquals(active, target.Session);
				var value = new { Requested = requested, SessionId = target.Id, Path = target.Path, Active = verified, PostconditionVerified = verified };
				return verified
					? McpJson.Write(ToolOutcome<object>.Ready(value))
					: McpJson.Write(ToolOutcome<object>.Degraded(value, "scene.activation_unverified", "The editor accepted scene activation, but SceneEditorSession.Active did not reach the requested session."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "scene.activation_failed", "Scene session activation failed.", ex)); }
		}

		[McpTool("pocketknife_dock_state")]
		public static string DockState(string? dockId = null, int limit = 50, int offset = 0)
		{
			try
			{
				if ((dockId?.Trim().Length ?? 0) > 200) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "editor.dock_id_too_long", "Dock ID must be 200 characters or fewer."));
				var boundedLimit = Math.Clamp(limit, 1, 100);
				var boundedOffset = Math.Max(offset, 0);
				var manager = GetDockManager();
				if (manager is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "editor.dock_manager_unavailable", "The loaded editor does not expose Editor.DockManager."));
				var types = EditorRuntime.GetMember(manager, "DockTypes") as IEnumerable;
				if (types is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "editor.dock_registry_unavailable", "The loaded editor does not expose its dock registry."));

				var all = new List<object>();
				foreach (var info in types)
				{
					var title = EditorRuntime.GetMember(info, "Title")?.ToString();
					if (string.IsNullOrWhiteSpace(title)) continue;
					if (!string.IsNullOrWhiteSpace(dockId) && !title.Equals(dockId.Trim(), StringComparison.OrdinalIgnoreCase)) continue;
					var open = ReadNullableBool(EditorRuntime.InvokeInstance(manager, "IsDockOpen", title));
					var widget = EditorRuntime.InvokeInstance(manager, "FindDockWidget", title);
					var active = widget is null ? (open == false ? false : null) : ReadNullableBool(widget, "IsFocused") ?? ReadNullableBool(widget, "IsActiveWindow");
					var raised = widget is null ? (open == false ? false : null) : ReadNullableBool(widget, "IsActiveWindow") ?? ReadNullableBool(widget, "IsFocused");
					var verified = open.HasValue && (open == false || (widget is not null && active.HasValue && raised.HasValue));
					all.Add(new { DockId = title, Title = title, Open = open, Active = active, Raised = raised, Verified = verified, Type = info.GetType().FullName });
				}
				if (!string.IsNullOrWhiteSpace(dockId) && all.Count == 0) return McpJson.Write(ToolOutcome<object>.Failure(new { Requested = dockId }, ToolStatus.Unavailable, "editor.dock_not_found", "No registered dock matched the requested dock ID."));
				var page = all.Skip(boundedOffset).Take(boundedLimit).ToArray();
				var pageVerified = page.All(item => (bool)item.GetType().GetProperty("Verified")!.GetValue(item)!);
				return McpJson.Write(ToolOutcome<object>.Ready(new { Count = page.Length, Total = all.Count, Offset = boundedOffset, Limit = boundedLimit, HasMore = boundedOffset + page.Length < all.Count, Docks = page, PostconditionVerified = pageVerified, StateSource = "Editor.DockManager" }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "editor.dock_state_failed", "Dock state discovery failed.", ex)); }
		}

		[McpTool("pocketknife_viewport_capture")]
		public static string ViewportCapture(string captureMode = "editor_viewport", int width = 1280, int height = 720, bool includeUi = false, string? fileName = null)
		{
			try
			{
				var mode = (captureMode ?? "").Trim().ToLowerInvariant();
				if (mode is not ("editor_viewport" or "scene_camera" or "play_first_person" or "play_third_person"))
					return McpJson.Write(ToolOutcome<object>.Failure(new { CaptureMode = captureMode }, ToolStatus.Failed, "capture.invalid_mode", "Capture mode is not supported.", remediation: "Use editor_viewport, scene_camera, play_first_person, or play_third_person."));
				width = Math.Clamp(width, 16, 4096);
				height = Math.Clamp(height, 16, 4096);
				if (mode is "play_first_person" or "play_third_person")
					return McpJson.Write(ToolOutcome<object>.Failure(new { CaptureMode = mode, Width = width, Height = height }, ToolStatus.Unavailable, "capture.play_camera_unavailable", "The loaded editor exposes no typed first-person or third-person camera surface.", remediation: "Use official camera_screenshot with a verified camera ID."));
				if (mode == "editor_viewport" && includeUi)
					return McpJson.Write(ToolOutcome<object>.Failure(new { CaptureMode = mode }, ToolStatus.Unavailable, "capture.editor_ui_unavailable", "Editor viewport UI capture is not exposed by the verified editor camera surface."));

				var playState = EditorRuntime.InvokeStatic("Editor.Mcp.PlayTools", "CurrentState");
				var playing = ReadNullableBool(playState, "IsPlaying") ?? false;
				object? bitmap = mode == "editor_viewport"
					? EditorRuntime.InvokeStatic("Editor.Mcp.SceneTools", "EditorCameraScreenshot", width, height)
					: EditorRuntime.InvokeStatic("Editor.Mcp.SceneTools", "CameraScreenshot", "", width, height, includeUi);
				if (bitmap is null) return McpJson.Write(ToolOutcome<object>.Failure(new { CaptureMode = mode, Width = width, Height = height, IsPlaying = playing }, ToolStatus.Unavailable, "capture.surface_unavailable", "The verified editor screenshot surface returned no bitmap."));

				var bytes = EncodePng(bitmap);
				if (bytes is null || bytes.Length == 0) return McpJson.Write(ToolOutcome<object>.Failure(new { CaptureMode = mode, Width = width, Height = height }, ToolStatus.Failed, "capture.encode_failed", "The editor returned a bitmap that could not be encoded as PNG."));
				var decoded = ReadPngDimensions(bytes);
				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 path = ResolveCapturePath(root, fileName, mode);
				if (path is null) return McpJson.Write(ToolOutcome<object>.Failure(new { CaptureMode = mode }, ToolStatus.Failed, "capture.path_invalid", "Capture output must remain inside the active project."));
				Directory.CreateDirectory(Path.GetDirectoryName(path)!);
				File.WriteAllBytes(path, bytes);
				var verified = File.Exists(path) && new FileInfo(path).Length == bytes.Length && decoded.Width == width && decoded.Height == height;
				var value = new
				{
					CaptureMode = mode,
					Width = width,
					Height = height,
					DecodedWidth = decoded.Width,
					DecodedHeight = decoded.Height,
					Bytes = bytes.Length,
					Path = path,
					IsPlaying = playing,
					IncludeUi = mode == "scene_camera" && includeUi,
					PostconditionVerified = verified,
					Image = new { type = "image", data = Convert.ToBase64String(bytes), mimeType = "image/png" }
				};
				return verified ? McpJson.Write(ToolOutcome<object>.Ready(value)) : McpJson.Write(ToolOutcome<object>.Failure(value, ToolStatus.Failed, "capture.verify_failed", "PNG output or decoded dimensions did not match the requested capture."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "capture.failed", "Viewport capture failed.", ex)); }
		}

		[McpTool("pocketknife_package_search")]
		public static string PackageSearch(string query, string? kind = null, int limit = 20, int offset = 0)
		{
			try
			{
				var normalized = (query ?? "").Trim();
				if (normalized.Length == 0) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "package.query_required", "A package query is required."));
				if (normalized.Length > 200) return McpJson.Write(ToolOutcome<object>.Failure(new { QueryLength = normalized.Length }, ToolStatus.Failed, "package.query_too_long", "Package query must be 200 characters or fewer."));
				var normalizedKind = (kind ?? "").Trim();
				if (normalizedKind.Length > 64 || (normalizedKind.Length > 0 && !normalizedKind.All(ch => char.IsLetterOrDigit(ch) || ch is '_' or '-')))
					return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "package.kind_invalid", "Package kind contains unsupported characters."));
				var backendQuery = string.IsNullOrWhiteSpace(normalizedKind) ? normalized : $"{normalized} type:{normalizedKind}";
				var boundedLimit = Math.Clamp(limit, 1, 100);
				var boundedOffset = Math.Max(offset, 0);
				var result = InvokeStaticAwait("Sandbox.Package", "FindAsync", 30000, backendQuery, boundedLimit, boundedOffset, CancellationToken.None);
				if (result is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "package.search_unavailable", "Sandbox.Package.FindAsync is unavailable or did not complete."));
				var packages = Enumerate(EditorRuntime.GetMember(result, "Packages") ?? EditorRuntime.GetMember(result, "Results")).ToArray();
				var total = ReadNullableInt(result, "TotalCount") ?? ReadNullableInt(result, "Total") ?? packages.Length;
				var items = packages.Select(PackagePayload).ToArray();
				return McpJson.Write(ToolOutcome<object>.Ready(new { Query = normalized, Kind = string.IsNullOrWhiteSpace(normalizedKind) ? null : normalizedKind, Count = items.Length, Total = total, Offset = boundedOffset, Limit = boundedLimit, HasMore = boundedOffset + items.Length < total, Source = "sbox.cloud", Provenance = "Sandbox.Package.FindAsync", Packages = items }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "package.search_failed", "Package search failed.", ex)); }
		}

		[McpTool("pocketknife_package_install")]
		public static string PackageInstall(string ident, string? version = null, bool includeDependencies = true)
		{
			try
			{
				var requestIdent = ResolvePackageIdent(ident, version, out var identError);
				if (requestIdent is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "package.ident_invalid", identError ?? "Package ident is invalid."));
				var package = FetchPackage(requestIdent);
				if (package is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Ident = requestIdent }, ToolStatus.Failed, "package.not_found", "The requested package could not be fetched."));
				var installed = InvokeStaticCompatible("Editor.AssetSystem", "IsCloudInstalled", requestIdent) as bool? ?? false;
				object? installedAsset = null;
				if (!installed)
				{
					var canInstall = InvokeStaticCompatible("Editor.AssetSystem", "CanCloudInstall", package) as bool?;
					if (canInstall == false) return McpJson.Write(ToolOutcome<object>.Failure(new { Ident = requestIdent }, ToolStatus.Unavailable, "package.install_unavailable", "The editor asset system does not permit cloud installation for this package."));
					var installTask = InvokeStaticCompatible("Editor.AssetSystem", "InstallAsync", requestIdent, false, null, CancellationToken.None);
					if (installTask is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Ident = requestIdent }, ToolStatus.Unavailable, "package.install_api_unavailable", "The verified editor package install API is unavailable."));
					installedAsset = AwaitValue(installTask, 120000);
				}
				object? revision = null;
				var deadline = DateTime.UtcNow.AddSeconds(120);
				while (DateTime.UtcNow < deadline)
				{
					installed = InvokeStaticCompatible("Editor.AssetSystem", "IsCloudInstalled", requestIdent) as bool? ?? false;
					revision = InvokeStaticCompatible("Editor.AssetSystem", "GetInstalledRevision", requestIdent);
					if (installed && revision is not null) break;
					Thread.Sleep(100);
				}
				var dependencies = DependencyIdList(package);
				var dependencyStates = dependencies.Select(dependency => new { Ident = dependency, Installed = (InvokeStaticCompatible("Editor.AssetSystem", "IsCloudInstalled", dependency) as bool?) ?? false }).ToArray();
				var dependenciesVerified = !includeDependencies || dependencyStates.All(item => item.Installed);
				var revisionText = RevisionText(revision);
				var verified = installed && revision is not null && dependenciesVerified;
				var value = new { Ident = requestIdent, Installed = installed, InstalledAsset = installedAsset is null ? null : AssetPayload(installedAsset), InstalledRevision = revisionText, IncludeDependencies = includeDependencies, Dependencies = dependencyStates, DependenciesVerified = dependenciesVerified, PostconditionVerified = verified };
				return verified ? McpJson.Write(ToolOutcome<object>.Ready(value)) : McpJson.Write(ToolOutcome<object>.Degraded(value, "package.install_unverified", "Package install completed or was dispatched, but installed revision/dependency readback is incomplete."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "package.install_failed", "Package installation failed.", ex)); }
		}

		[McpTool("pocketknife_package_status")]
		public static string PackageStatus(string ident, string? version = null)
		{
			try
			{
				var requestIdent = ResolvePackageIdent(ident, version, out var identError);
				if (requestIdent is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "package.ident_invalid", identError ?? "Package ident is invalid."));
				var package = FetchPackage(requestIdent);
				var installed = InvokeStaticCompatible("Editor.AssetSystem", "IsCloudInstalled", requestIdent) as bool?;
				var revision = InvokeStaticCompatible("Editor.AssetSystem", "GetInstalledRevision", requestIdent);
				if (package is null || !installed.HasValue) return McpJson.Write(ToolOutcome<object>.Failure(new { Ident = requestIdent }, ToolStatus.Unavailable, "package.status_unavailable", "Package metadata or installed revision state is unavailable."));
				return McpJson.Write(ToolOutcome<object>.Ready(new { Ident = requestIdent, Installed = installed.Value, InstalledRevision = RevisionText(revision), Package = PackagePayload(package), Source = "Editor.AssetSystem", Provenance = "Sandbox.Package.FetchAsync + Editor.AssetSystem.GetInstalledRevision" }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "package.status_failed", "Package status lookup failed.", ex)); }
		}

		[McpTool("pocketknife_asset_search")]
		public static string AssetSearch(string query, string? kind = null, string? pathPrefix = null, int limit = 20, int offset = 0)
		{
			try
			{
				var normalized = (query ?? "").Trim();
				if (normalized.Length > 200) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "asset.query_too_long", "Asset query must be 200 characters or fewer."));
				var normalizedKind = (kind ?? "").Trim().ToLowerInvariant();
				var allowedKinds = new[] { "hdri", "model", "texture", "material", "sound", "sprite", "map" };
				if (!string.IsNullOrWhiteSpace(normalizedKind) && !allowedKinds.Contains(normalizedKind, StringComparer.Ordinal)) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "asset.kind_invalid", "Asset kind is not supported."));
				var prefix = ToolGuard.Normalize(pathPrefix ?? "");
				if (prefix.Length > 200) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "asset.path_prefix_too_long", "Asset path prefix must be 200 characters or fewer."));
				var entries = new List<AssetSearchEntry>();
				var all = EditorRuntime.GetStaticProperty("Editor.AssetSystem", "All");
				foreach (var asset in Enumerate(all))
				{
					var assetPath = ReadText(asset, "Path", "ResourcePath", "Name") ?? "";
					var assetType = ReadText(asset, "TypeName", "Type", "AssetType") ?? "";
					var assetTags = string.Join(" ", Enumerate(EditorRuntime.GetMember(asset, "Tags")).Select(value => value?.ToString()));
					entries.Add(new AssetSearchEntry(assetPath, assetType, assetTags, AssetPayload(asset)));
				}
				var installedPackages = InvokeStaticCompatible("Editor.AssetSystem", "GetInstalledPackages");
				foreach (var package in Enumerate(installedPackages))
				{
					try
					{
						var packageIdent = ReadText(package, "FullIdent", "Ident") ?? "";
						foreach (var file in Enumerate(InvokeStaticCompatible("Editor.AssetSystem", "GetPackageFiles", package)))
						{
							var filePath = file.ToString() ?? "";
							if (filePath.Length > 0) entries.Add(new AssetSearchEntry(filePath, Path.GetExtension(filePath), "", PackageFilePayload(packageIdent, filePath)));
						}
					}
					catch { }
				}
				if (all is null && installedPackages is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "asset.index_unavailable", "Editor asset and installed-package indexes are unavailable."));
				var terms = normalized.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
				var matches = entries.Where(entry => terms.All(term => string.Join(" ", entry.Path, entry.Type, entry.Tags).Contains(term, StringComparison.OrdinalIgnoreCase)))
					.Where(entry => string.IsNullOrWhiteSpace(prefix) || ToolGuard.Normalize(entry.Path).StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
					.Where(entry => KindMatches(normalizedKind, entry.Path, entry.Type)).Select(entry => entry.Payload).ToArray();
				var boundedLimit = Math.Clamp(limit, 1, 100);
				var boundedOffset = Math.Max(offset, 0);
				var page = matches.Skip(boundedOffset).Take(boundedLimit).ToArray();
				return McpJson.Write(ToolOutcome<object>.Ready(new { Query = normalized, Kind = string.IsNullOrWhiteSpace(normalizedKind) ? null : normalizedKind, PathPrefix = string.IsNullOrWhiteSpace(prefix) ? null : prefix, Count = page.Length, Total = matches.Length, Offset = boundedOffset, Limit = boundedLimit, HasMore = boundedOffset + page.Length < matches.Length, Source = "Editor.AssetSystem.All + Editor.AssetSystem.GetInstalledPackages", Provenance = "editor asset index plus installed cloud package files", Assets = page }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "asset.search_failed", "Editor asset search failed.", ex)); }
		}

		[McpTool("pocketknife_asset_info")]
		public static string AssetInfo(string pathOrIdent)
		{
			try
			{
				var requested = (pathOrIdent ?? "").Trim();
				if (requested.Length == 0 || requested.Length > 200 || requested.Contains("://", StringComparison.Ordinal)) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "asset.ident_invalid", "Asset path or package ident is invalid."));
				var asset = InvokeStaticCompatible("Editor.AssetSystem", "FindByPath", requested);
				if (asset is not null) return McpJson.Write(ToolOutcome<object>.Ready(new { Requested = requested, Asset = AssetPayload(asset), Source = "Editor.AssetSystem.FindByPath", Provenance = "editor asset index" }));
				var package = requested.Contains('.') && !requested.Contains('/') && !requested.Contains('\\') ? FetchPackage(requested) : null;
				if (package is not null) return McpJson.Write(ToolOutcome<object>.Ready(new { Requested = requested, Package = PackagePayload(package), Source = "Sandbox.Package.FetchAsync", Provenance = "sbox.cloud package metadata" }));
				return McpJson.Write(ToolOutcome<object>.Failure(new { Requested = requested }, ToolStatus.Unavailable, "asset.not_found", "No indexed asset or package matched the requested path or ident."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "asset.info_failed", "Asset information lookup failed.", ex)); }
		}

		[McpTool("pocketknife_editor_surface_status")]
		public static string EditorSurfaceStatus(string surface)
		{
			try
			{
				var descriptor = GetSurfaceDescriptor(surface);
				if (descriptor is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Surface = surface }, ToolStatus.Failed, "surface.invalid", "Editor surface is not allowlisted."));
				var found = descriptor.RequiredTypes.Select(EditorRuntime.FindType).FirstOrDefault(type => type is not null);
				var runtimeAvailable = found is not null || descriptor.Surface == "material" && EditorRuntime.FindType("Editor.EditorUtility") is not null;
				var documentApiAvailable = descriptor.DocumentApiTypes.Any(type => EditorRuntime.FindType(type) is not null && descriptor.DocumentMethods.Any(method => HasMethod(EditorRuntime.FindType(type), method)));
				var value = new { Surface = descriptor.Surface, RuntimeAvailable = runtimeAvailable, RuntimeType = found?.FullName, RequiredType = descriptor.RequiredTypes.FirstOrDefault(), RequiredComponent = descriptor.RequiredComponent, OfficialMatchingTools = descriptor.OfficialTools, DocumentApiAvailable = documentApiAvailable, MissingDependency = runtimeAvailable ? (documentApiAvailable ? null : descriptor.DocumentApiMissingCode) : descriptor.MissingCode };
				if (!runtimeAvailable) return McpJson.Write(ToolOutcome<object>.Failure(value, ToolStatus.Unavailable, descriptor.MissingCode, descriptor.MissingMessage));
				return McpJson.Write(ToolOutcome<object>.Ready(value));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "surface.status_failed", "Editor surface status lookup failed.", ex)); }
		}

		[McpTool("pocketknife_editor_document_open")]
		public static string EditorDocumentOpen(string surface, string path)
		{
			try
			{
				var descriptor = GetSurfaceDescriptor(surface);
				if (descriptor is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Surface = surface }, ToolStatus.Failed, "document.surface_invalid", "Editor document surface is not allowlisted."));
				var status = descriptor.RequiredTypes.Select(EditorRuntime.FindType).FirstOrDefault(type => type is not null);
				if (status is null && descriptor.Surface != "material") return McpJson.Write(ToolOutcome<object>.Failure(new { Surface = descriptor.Surface }, ToolStatus.Unavailable, descriptor.MissingCode, descriptor.MissingMessage));
				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 normalizedPath = ToolGuard.ResolveAssetPath(path);
				if (!IsUnder(normalizedPath, root) || !File.Exists(normalizedPath)) return McpJson.Write(ToolOutcome<object>.Failure(new { Surface = descriptor.Surface, Path = normalizedPath }, ToolStatus.Failed, "document.path_invalid", "Document path must exist inside the active project."));
				EditorRuntime.InvokeStatic("Editor.EditorUtility", "OpenFile", normalizedPath);
				var verified = false;
				for (var i = 0; i < 10 && !verified; i++) { Thread.Sleep(100); verified = SurfaceDockReadback(descriptor); }
				var value = new { Surface = descriptor.Surface, Path = normalizedPath, Requested = true, PostconditionVerified = verified };
				return verified ? McpJson.Write(ToolOutcome<object>.Ready(value)) : McpJson.Write(ToolOutcome<object>.Degraded(value, "document.open_unverified", "OpenFile accepted the request, but active surface state readback did not confirm the requested document."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "document.open_failed", "Editor document open failed.", ex)); }
		}

		[McpTool("pocketknife_navmesh_status")]
		public static string NavMeshStatus(string? scenePath = null)
		{
			try
			{
				var snapshot = ResolveSceneSnapshot(scenePath);
				if (snapshot is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Requested = scenePath }, ToolStatus.Unavailable, "scene.session_not_found", "No open scene session matched the requested scene."));
				var navMesh = GetNavMesh(snapshot);
				if (navMesh is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name, Path = snapshot.Path }, ToolStatus.Unavailable, "navmesh.not_present", "The selected scene does not expose Sandbox.Navigation.NavMesh."));
				return McpJson.Write(ToolOutcome<object>.Ready(new { Scene = snapshot.Name, Path = snapshot.Path, IsEnabled = ReadNullableBool(navMesh, "IsEnabled"), IsGenerating = ReadNullableBool(navMesh, "IsGenerating"), IsDirty = ReadNullableBool(navMesh, "IsDirty"), Dependency = "Sandbox.Navigation.NavMesh", PostconditionVerified = true }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "navmesh.status_failed", "NavMesh status lookup failed.", ex)); }
		}

		[McpTool("pocketknife_navmesh_bake")]
		public static string NavMeshBake(string? scenePath = null)
		{
			try
			{
				var snapshot = ResolveSceneSnapshot(scenePath);
				if (snapshot is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Requested = scenePath }, ToolStatus.Unavailable, "scene.session_not_found", "No open scene session matched the requested scene."));
				if (snapshot.Playing) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name }, ToolStatus.Unavailable, "navmesh.playing", "NavMesh bake requires a non-playing scene session."));
				var navMesh = GetNavMesh(snapshot);
				if (navMesh is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name }, ToolStatus.Unavailable, "navmesh.not_present", "The selected scene does not expose Sandbox.Navigation.NavMesh."));
				var before = ReadNavMeshState(navMesh);
				if (before.IsEnabled == false) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name, Before = before }, ToolStatus.Unavailable, "navmesh.disabled", "The selected NavMesh is disabled; no bake was attempted."));
				if (before.IsGenerating == false && before.IsDirty == false) return McpJson.Write(ToolOutcome<object>.Ready(new { Scene = snapshot.Name, Before = before, After = before, NoOp = true, PostconditionVerified = true }));
				if (!HasMethod(navMesh.GetType(), "BakeNavMesh")) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name, Before = before }, ToolStatus.Unavailable, "navmesh.bake_api_unavailable", "Sandbox.Navigation.NavMesh.BakeNavMesh is unavailable."));
				EditorRuntime.InvokeInstance(navMesh, "BakeNavMesh");
				var deadline = DateTime.UtcNow.AddSeconds(120);
				var sawGeneration = before.IsGenerating == true;
				NavMeshSnapshot after = before;
				while (DateTime.UtcNow < deadline)
				{
					Thread.Sleep(100);
					after = ReadNavMeshState(navMesh);
					if (after.IsGenerating == true) sawGeneration = true;
					if (after.IsGenerating == false && after.IsDirty == false && (sawGeneration || before.IsDirty == true)) break;
				}
				var verified = after.IsGenerating == false && after.IsDirty == false && (sawGeneration || before.IsDirty == true);
				var value = new { Scene = snapshot.Name, Path = snapshot.Path, Before = before, After = after, PostconditionVerified = verified };
				return verified ? McpJson.Write(ToolOutcome<object>.Ready(value)) : McpJson.Write(ToolOutcome<object>.Degraded(value, "navmesh.timeout", "NavMesh bake did not reach a clean completed state before timeout."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "navmesh.bake_failed", "NavMesh bake failed.", ex)); }
		}

		[McpTool("pocketknife_navmesh_path")]
		public static string NavMeshPath(Vector3 start, Vector3 end, int maxPoints = 256)
		{
			try
			{
				var boundedMax = Math.Clamp(maxPoints, 1, 256);
				var snapshot = ResolveSceneSnapshot(null);
				if (snapshot is null) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Unavailable, "scene.session_not_found", "No active scene session is available."));
				var navMesh = GetNavMesh(snapshot);
				if (navMesh is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name }, ToolStatus.Unavailable, "navmesh.not_present", "The active scene does not expose Sandbox.Navigation.NavMesh."));
				if (ReadNullableBool(navMesh, "IsEnabled") == false) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name }, ToolStatus.Unavailable, "navmesh.disabled", "The active NavMesh is disabled; path calculation was not attempted."));
				var path = EditorRuntime.InvokeInstance(navMesh, "GetSimplePath", start, end);
				if (path is null) return McpJson.Write(ToolOutcome<object>.Failure(new { Scene = snapshot.Name }, ToolStatus.Unavailable, "navmesh.path_api_unavailable", "Sandbox.Navigation.NavMesh.GetSimplePath is unavailable."));
				var points = Enumerate(path).ToArray();
				var page = points.Take(boundedMax).Select(point => point?.ToString() ?? "").ToArray();
				return McpJson.Write(ToolOutcome<object>.Ready(new { Scene = snapshot.Name, Start = start.ToString(), End = end.ToString(), Count = page.Length, Total = points.Length, MaxPoints = boundedMax, HasMore = points.Length > page.Length, Points = page, PostconditionVerified = true }));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "navmesh.path_failed", "NavMesh path query failed.", ex)); }
		}

		[McpTool("pocketknife_hotload_await")]
		public static string HotloadAwait(string? packageIdent = null, int timeoutMs = 120000)
		{
			try
			{
				if (!string.IsNullOrWhiteSpace(packageIdent) && packageIdent.Trim().Length > 200) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "hotload.package_invalid", "Package ident must be 200 characters or fewer."));
				var boundedTimeout = Math.Clamp(timeoutMs, 1000, 120000);
				var manager = GetStaticObject("Sandbox.HotloadManager", "Current") ?? GetStaticObject("Sandbox.HotloadManager", "Instance");
				if (manager is null) return McpJson.Write(ToolOutcome<object>.Failure(new { PackageIdent = packageIdent }, ToolStatus.Unavailable, "hotload.api_unavailable", "The loaded editor does not expose a current HotloadManager instance."));
				var baseline = HotloadRevision(manager);
				var started = DateTime.UtcNow;
				var needsSwap = ReadNullableBool(manager, "NeedsSwap") ?? false;
				while (needsSwap && DateTime.UtcNow - started < TimeSpan.FromMilliseconds(boundedTimeout)) { Thread.Sleep(100); needsSwap = ReadNullableBool(manager, "NeedsSwap") ?? true; }
				var observed = HotloadRevision(manager);
				var verified = !needsSwap;
				var value = new { PackageIdent = string.IsNullOrWhiteSpace(packageIdent) ? null : packageIdent.Trim(), BaselineRevision = baseline, ObservedRevision = observed, ElapsedMs = (long)(DateTime.UtcNow - started).TotalMilliseconds, PostconditionVerified = verified };
				return verified ? McpJson.Write(ToolOutcome<object>.Ready(value)) : McpJson.Write(ToolOutcome<object>.Degraded(value, "hotload.await_timeout", "Hotload remained pending until timeout."));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "hotload.await_failed", "Hotload await failed.", ex)); }
		}

		[McpTool("pocketknife_network_status")]
		public static string NetworkStatus()
		{
			try
			{
				var gameType = EditorRuntime.FindType("Sandbox.Game");
				var lobbyApi = FindFirstType("Sandbox.Lobby", "Sandbox.Multiplayer", "Sandbox.LobbyService", "Editor.LobbyService");
				var providerConfigured = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("VOYAGEUR_PROVIDER_ENDPOINT"));
				return McpJson.Write(ToolOutcome<object>.Ready(new
				{
					RuntimeAvailable = gameType is not null,
					InGame = ReadStaticBool(gameType, "InGame"),
					Playing = ReadStaticBool(gameType, "IsPlaying"),
					Paused = ReadStaticBool(gameType, "IsPaused"),
					TypedLobbyApiAvailable = lobbyApi is not null,
					VoyageurProviderConfigured = providerConfigured,
					Redacted = true
				}));
			}
			catch (Exception ex) { return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "network.status_failed", "Network status lookup failed.", ex)); }
		}

		[McpTool("pocketknife_lobby_start")]
		public static string LobbyStart(string? scenePath = null, int maxPlayers = 0)
		{
			return LobbyOperation("start", scenePath, maxPlayers);
		}

		[McpTool("pocketknife_lobby_stop")]
		public static string LobbyStop()
		{
			return LobbyOperation("stop", null, 0);
		}

		[McpTool("pocketknife_server_start")]
		public static string ServerStart(string provider = "voyageur", string? projectPath = null, string? scenePath = null)
		{
			if (!string.Equals(provider?.Trim(), "voyageur", StringComparison.OrdinalIgnoreCase)) return McpJson.Write(ToolOutcome<object>.Failure(new { Provider = provider }, ToolStatus.Failed, "server.provider_invalid", "Only the configured voyageur provider is supported."));
			return McpJson.Write(ToolOutcome<object>.Failure(new { Provider = "voyageur", ProjectPath = projectPath, ScenePath = scenePath }, ToolStatus.Unavailable, "server.provider_unavailable", "No typed Voyageur editor provider is available; no process was launched."));
		}

		[McpTool("pocketknife_server_status")]
		public static string ServerStatus(string operationId)
		{
			if (string.IsNullOrWhiteSpace(operationId) || operationId.Trim().Length > 200) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "server.operation_invalid", "A provider operation ID of at most 200 characters is required."));
			return McpJson.Write(ToolOutcome<object>.Failure(new { OperationId = operationId.Trim() }, ToolStatus.Unavailable, "server.provider_unavailable", "No typed Voyageur editor provider is available."));
		}

		[McpTool("pocketknife_server_stop")]
		public static string ServerStop(string operationId)
		{
			if (string.IsNullOrWhiteSpace(operationId) || operationId.Trim().Length > 200) return McpJson.Write(ToolOutcome<object>.Failure(null, ToolStatus.Failed, "server.operation_invalid", "A provider operation ID of at most 200 characters is required."));
			return McpJson.Write(ToolOutcome<object>.Failure(new { OperationId = operationId.Trim() }, ToolStatus.Unavailable, "server.provider_unavailable", "No typed Voyageur editor provider is available; no process was stopped."));
		}

		private sealed class SceneSnapshot
		{
			public required object Session { get; init; }
			public required string Id { get; init; }
			public string? Path { get; init; }
			public string Name { get; init; } = "";
			public bool Active { get; init; }
			public bool Dirty { get; init; }
			public bool Mounted { get; init; }
			public bool Playing { get; init; }
		}

		private sealed record AssetSearchEntry(string Path, string Type, string Tags, object Payload);
		private sealed record NavMeshSnapshot(bool? IsEnabled, bool? IsGenerating, bool? IsDirty);

		private sealed record SurfaceDescriptor(string Surface, string[] RequiredTypes, string? RequiredComponent, string[] OfficialTools, string MissingCode, string MissingMessage, string[] DocumentApiTypes, string[] DocumentMethods, string DocumentApiMissingCode, string[] DockTokens);

		private static readonly SurfaceDescriptor[] SurfaceDescriptors =
		{
			new("material", new[] { "Editor.AssetSystem" }, null, new[] { "asset_info", "asset_write" }, "surface.material_unavailable", "The material editor asset surface is unavailable.", new[] { "Editor.AssetSystem" }, new[] { "FindByPath" }, "surface.material_document_api_unavailable", new[] { "material", "material editor" }),
			new("modeldoc", new[] { "Editor.ModelEditor.ModelDoc", "Editor.ModelDocParser" }, null, Array.Empty<string>(), "surface.modeldoc_unavailable", "ModelDoc runtime surface is unavailable.", new[] { "Editor.ModelEditor.ModelDoc" }, Array.Empty<string>(), "surface.modeldoc_document_api_unavailable", new[] { "modeldoc", "model editor" }),
			new("hammer", new[] { "Editor.MapEditor.HammerSceneEditorSession" }, null, Array.Empty<string>(), "surface.hammer_unavailable", "Hammer runtime surface is unavailable.", new[] { "Editor.MapEditor.HammerSceneEditorSession" }, Array.Empty<string>(), "surface.hammer_document_api_unavailable", new[] { "hammer" }),
			new("shadergraph", new[] { "Editor.ShaderGraph.BaseNode" }, null, Array.Empty<string>(), "surface.shadergraph_unavailable", "ShaderGraph runtime surface is unavailable.", new[] { "Editor.ShaderGraph.GraphCompiler" }, Array.Empty<string>(), "surface.shadergraph_document_api_unavailable", new[] { "shadergraph", "shader graph" }),
			new("sprite", new[] { "Editor.SpriteEditor.SpritesheetImporter" }, null, Array.Empty<string>(), "surface.sprite_unavailable", "Sprite runtime surface is unavailable.", new[] { "Editor.SpriteEditor.SpritesheetImporter" }, Array.Empty<string>(), "surface.sprite_document_api_unavailable", new[] { "sprite" }),
			new("hotspot", new[] { "Editor.HotspotEditor", "Editor.Hotspot" }, null, Array.Empty<string>(), "surface.hotspot_unavailable", "Hotspot runtime surface is unavailable.", Array.Empty<string>(), Array.Empty<string>(), "surface.hotspot_document_api_unavailable", new[] { "hotspot" }),
			new("terrain", new[] { "Sandbox.TerrainComponent", "Editor.Terrain" }, "TerrainComponent", Array.Empty<string>(), "surface.terrain_unavailable", "Terrain runtime component/editor surface is unavailable.", Array.Empty<string>(), Array.Empty<string>(), "surface.terrain_document_api_unavailable", new[] { "terrain" }),
			new("navmesh", new[] { "Sandbox.Navigation.NavMesh" }, "Sandbox.Navigation.NavMesh", Array.Empty<string>(), "surface.navmesh_unavailable", "NavMesh runtime surface is unavailable.", new[] { "Sandbox.Navigation.NavMesh" }, new[] { "BakeNavMesh", "GetSimplePath" }, "surface.navmesh_document_api_unavailable", new[] { "navmesh", "navigation" })
		};

		private static SurfaceDescriptor? GetSurfaceDescriptor(string? surface) => SurfaceDescriptors.FirstOrDefault(item => item.Surface.Equals((surface ?? "").Trim().ToLowerInvariant(), StringComparison.Ordinal));

		private static object? GetDockManager()
		{
			var main = GetStaticObject("Editor.EditorMainWindow", "Current");
			return EditorRuntime.GetMember(main, "DockManager");
		}

		private static IEnumerable<SceneSnapshot> GetSceneSnapshots()
		{
			var all = EditorRuntime.GetStaticProperty("Editor.SceneEditorSession", "All");
			var active = EditorRuntime.GetStaticProperty("Editor.SceneEditorSession", "Active");
			var index = 0;
			foreach (var session in Enumerate(all))
			{
				var scene = EditorRuntime.GetMember(session, "Scene");
				var path = ReadText(scene, "ResourcePath", "FilePath", "Path", "SourcePath");
				path ??= ReadText(EditorRuntime.GetMember(scene, "Source"), "ResourcePath", "FilePath", "Path", "SourcePath");
				var name = ReadText(scene, "Name", "Title") ?? ReadText(session, "Name") ?? $"Scene {index + 1}";
				var id = !string.IsNullOrWhiteSpace(path) ? "scene:" + ToolGuard.Normalize(path) : "untitled:" + index;
				yield return new SceneSnapshot
				{
					Session = session,
					Id = id,
					Path = string.IsNullOrWhiteSpace(path) ? null : ToolGuard.Normalize(path),
					Name = name,
					Active = ReferenceEquals(session, active),
					Dirty = ReadNullableBool(session, "HasUnsavedChanges") ?? false,
					Mounted = ReadNullableBool(session, "IsMounted") ?? false,
					Playing = ReadNullableBool(session, "IsPlaying") ?? false
				};
				index++;
			}
		}

		private static SceneSnapshot? ResolveSceneSnapshot(string? requested)
		{
			var snapshots = GetSceneSnapshots().ToArray();
			if (snapshots.Length == 0) return null;
			if (string.IsNullOrWhiteSpace(requested)) return snapshots.FirstOrDefault(item => item.Active) ?? snapshots[0];
			var value = requested.Trim();
			return snapshots.FirstOrDefault(item => item.Id.Equals(value, StringComparison.OrdinalIgnoreCase) || item.Name.Equals(value, StringComparison.OrdinalIgnoreCase) || (!string.IsNullOrWhiteSpace(item.Path) && item.Path.Equals(ToolGuard.Normalize(value), StringComparison.OrdinalIgnoreCase)));
		}

		private static object? GetNavMesh(SceneSnapshot snapshot)
		{
			var scene = EditorRuntime.GetMember(snapshot.Session, "Scene");
			var navMesh = EditorRuntime.GetMember(scene, "NavMesh");
			return navMesh?.GetType().FullName?.Contains("Sandbox.Navigation.NavMesh", StringComparison.Ordinal) == true ? navMesh : null;
		}

		private static NavMeshSnapshot ReadNavMeshState(object navMesh) => new(ReadNullableBool(navMesh, "IsEnabled"), ReadNullableBool(navMesh, "IsGenerating"), ReadNullableBool(navMesh, "IsDirty"));

		private static object? FetchPackage(string ident)
		{
			return InvokeStaticAwait("Sandbox.Package", "FetchAsync", 30000, ident, false, true) ?? InvokeStaticAwait("Sandbox.Package", "FetchAsync", 30000, ident, false);
		}

		private static string? ResolvePackageIdent(string ident, string? version, out string? error)
		{
			error = null;
			var normalized = (ident ?? "").Trim();
			if (normalized.Length == 0 || normalized.Length > 200 || normalized.Contains("://", StringComparison.Ordinal) || normalized.Contains('/') || normalized.Contains('\\')) { error = "Package ident is invalid."; return null; }
			if (string.IsNullOrWhiteSpace(version)) return normalized;
			if (!int.TryParse(version.Trim(), out var revision) || revision < 0) { error = "Package version must be a non-negative numeric revision."; return null; }
			var separator = normalized.IndexOf('.');
			if (separator <= 0 || separator == normalized.Length - 1) { error = "Versioned package ident must contain org and package segments."; return null; }
			var formatted = EditorRuntime.InvokeStatic("Sandbox.Package", "FormatIdent", normalized[..separator], normalized[(separator + 1)..], (int?)revision, false)?.ToString();
			if (string.IsNullOrWhiteSpace(formatted)) { error = "The loaded package API could not format a versioned ident."; return null; }
			return formatted;
		}

		private static object PackagePayload(object package) => new
		{
			Ident = ReadText(package, "FullIdent", "Ident") ?? "",
			Title = ReadText(package, "Title") ?? "",
			Author = ReadText(EditorRuntime.GetMember(package, "Org"), "Title", "Ident") ?? ReadText(package, "Org") ?? "",
			Type = ReadText(package, "TypeName", "PackageType", "Type") ?? "",
			Version = RevisionText(EditorRuntime.GetMember(package, "Revision")),
			Source = "sbox.cloud",
			Provenance = "Sandbox.Package"
		};

		private static object AssetPayload(object asset)
		{
			var path = ReadText(asset, "Path", "ResourcePath", "Name") ?? "";
			var package = EditorRuntime.GetMember(asset, "Package") ?? EditorRuntime.GetMember(asset, "CloudPackage");
			var packageIdent = ReadText(package, "FullIdent", "Ident");
			var source = package is not null || ReadNullableBool(asset, "IsCloud") == true ? "cloud" : "project";
			return new
			{
				Path = path,
				Type = ReadText(asset, "TypeName", "Type", "AssetType") ?? "",
				Package = packageIdent,
				Source = source,
				Provenance = packageIdent is null ? "Editor.AssetSystem.All" : "Editor.AssetSystem.All + Sandbox.Package",
				Compiled = ReadNullableBool(asset, "IsCompiled"),
				CompileFailed = ReadNullableBool(asset, "IsCompileFailed"),
				UpToDate = ReadNullableBool(asset, "IsCompiledAndUpToDate"),
				AvailableEditorSurface = SurfaceForPath(path)
			};
		}

		private static object PackageFilePayload(string packageIdent, string path) => new
		{
			Path = path,
			Type = Path.GetExtension(path),
			Package = packageIdent,
			Source = "cloud",
			Provenance = "Editor.AssetSystem.GetPackageFiles"
		};

		private static string? SurfaceForPath(string path)
		{
			var extension = Path.GetExtension(path).ToLowerInvariant();
			return extension switch
			{
				".vmat" => "material",
				".vmdl" => "modeldoc",
				".vmap" => "hammer",
				".sprite" => "sprite",
				".hotspot" => "hotspot",
				".scene" => "scene",
				_ => null
			};
		}

		private static string? RevisionText(object? revision) => revision is null ? null : ReadText(revision, "VersionId", "AssetVersionId", "Version", "Revision", "Id") ?? revision.ToString();

		private static string[] DependencyIdList(object package) => Enumerate(EditorRuntime.GetMember(package, "PackageReferences")).Select(value => ReadText(value, "FullIdent", "Ident") ?? value?.ToString()).Where(value => !string.IsNullOrWhiteSpace(value)).Cast<string>().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();

		private static string? ResolveCapturePath(string root, string? fileName, string mode)
		{
			var captureRoot = Path.GetFullPath(Path.Combine(root, ".sbox", "captures"));
			var requested = string.IsNullOrWhiteSpace(fileName) ? $"{mode}.png" : fileName.Trim();
			if (requested.Length > 200) return null;
			var path = Path.IsPathRooted(requested) ? Path.GetFullPath(requested) : Path.GetFullPath(Path.Combine(captureRoot, requested));
			return IsUnder(path, root) ? path : null;
		}

		private static byte[]? EncodePng(object bitmap)
		{
			var method = bitmap.GetType().GetMethod("ToPng", BindingFlags.Public | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null);
			return method?.Invoke(bitmap, null) as byte[];
		}

		private static (int Width, int Height) ReadPngDimensions(byte[] bytes)
		{
			if (bytes.Length < 24 || bytes[0] != 137 || bytes[1] != 80 || bytes[2] != 78 || bytes[3] != 71) return (0, 0);
			var width = (bytes[16] << 24) | (bytes[17] << 16) | (bytes[18] << 8) | bytes[19];
			var height = (bytes[20] << 24) | (bytes[21] << 16) | (bytes[22] << 8) | bytes[23];
			return (width, height);
		}

		private static bool KindMatches(string kind, string path, string type)
		{
			if (string.IsNullOrWhiteSpace(kind)) return true;
			var extension = Path.GetExtension(path).TrimStart('.').ToLowerInvariant();
			var value = string.Join(" ", path, type).ToLowerInvariant();
			return kind switch
			{
				"hdri" => extension is "hdr" or "exr" || value.Contains("hdri"),
				"model" => extension is "vmdl" or "fbx" or "obj" || value.Contains("model"),
				"texture" => extension is "vtex" or "png" or "tga" or "jpg" or "jpeg" || value.Contains("texture"),
				"material" => extension is "vmat" || value.Contains("material"),
				"sound" => extension is "sound" or "wav" or "mp3" or "ogg" || value.Contains("sound"),
				"sprite" => extension is "sprite" or "vtex" || value.Contains("sprite"),
				"map" => extension is "vmap" or "scene" || value.Contains("map"),
				_ => false
			};
		}

		private static bool SurfaceDockReadback(SurfaceDescriptor descriptor)
		{
			var manager = GetDockManager();
			if (manager is null) return false;
			var types = EditorRuntime.GetMember(manager, "DockTypes") as IEnumerable;
			if (types is null) return false;
			foreach (var info in types)
			{
				var title = EditorRuntime.GetMember(info, "Title")?.ToString() ?? "";
				if (!descriptor.DockTokens.Any(token => title.Contains(token, StringComparison.OrdinalIgnoreCase))) continue;
				var open = ReadNullableBool(EditorRuntime.InvokeInstance(manager, "IsDockOpen", title));
				var widget = EditorRuntime.InvokeInstance(manager, "FindDockWidget", title);
				var active = widget is not null && (ReadNullableBool(widget, "IsActiveWindow") == true || ReadNullableBool(widget, "IsFocused") == true);
				if (open == true && active) return true;
			}
			return false;
		}

		private static string LobbyOperation(string operation, string? scenePath, int maxPlayers)
		{
			if (maxPlayers < 0 || maxPlayers > 256) return McpJson.Write(ToolOutcome<object>.Failure(new { MaxPlayers = maxPlayers }, ToolStatus.Failed, "lobby.max_players_invalid", "MaxPlayers must be between 0 and 256."));
			var api = FindFirstType("Sandbox.Lobby", "Sandbox.Multiplayer", "Sandbox.LobbyService", "Editor.LobbyService");
			return McpJson.Write(ToolOutcome<object>.Failure(new { Operation = operation, ScenePath = scenePath, MaxPlayers = maxPlayers, TypedApi = api?.FullName }, ToolStatus.Unavailable, "lobby.api_unavailable", "The current s&box runtime exposes no typed lobby lifecycle API; no lobby action was dispatched."));
		}

		private static Type? FindFirstType(params string[] names) => names.Select(EditorRuntime.FindType).FirstOrDefault(type => type is not null);

		private static bool? ReadStaticBool(Type? type, string name)
		{
			if (type is null) return null;
			try { return type.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) as bool?; } catch { return null; }
		}

		private static object? GetStaticObject(string typeName, string memberName) => EditorRuntime.GetStaticProperty(typeName, memberName) ?? EditorRuntime.GetStaticField(typeName, memberName);

		private static string? ReadText(object? target, params string[] names)
		{
			foreach (var name in names)
			{
				var value = EditorRuntime.GetMember(target, name);
				if (value is null) continue;
				var text = value.ToString();
				if (!string.IsNullOrWhiteSpace(text)) return text;
			}
			return null;
		}

		private static bool? ReadNullableBool(object? target, string name)
		{
			var value = EditorRuntime.GetMember(target, name);
			return value is bool boolean ? boolean : null;
		}

		private static bool? ReadNullableBool(object? value) => value is bool boolean ? boolean : null;

		private static int? ReadNullableInt(object? target, string name)
		{
			var value = EditorRuntime.GetMember(target, name);
			try { return value is null ? null : Convert.ToInt32(value); } catch { return null; }
		}

		private static IEnumerable<object> Enumerate(object? value)
		{
			if (value is not IEnumerable enumerable) yield break;
			foreach (var item in enumerable) if (item is not null) yield return item;
		}

		private static bool HasMethod(Type? type, string name) => type?.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).Any(method => method.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) == true;

		private static object? InvokeStaticCompatible(string typeName, string methodName, params object?[] arguments)
		{
			var type = EditorRuntime.FindType(typeName);
			var method = FindCompatibleMethod(type, methodName, arguments, staticOnly: true);
			return method?.Invoke(null, arguments);
		}

		private static MethodInfo? FindCompatibleMethod(Type? type, string methodName, object?[] arguments, bool staticOnly)
		{
			if (type is null) return null;
			var flags = BindingFlags.Public | BindingFlags.NonPublic | (staticOnly ? BindingFlags.Static : BindingFlags.Instance);
			return type.GetMethods(flags).FirstOrDefault(method => method.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase) && method.GetParameters().Length == arguments.Length && method.GetParameters().Zip(arguments, ParameterAccepts).All(result => result));
		}

		private static bool ParameterAccepts(ParameterInfo parameter, object? value)
		{
			if (value is null) return !parameter.ParameterType.IsValueType || Nullable.GetUnderlyingType(parameter.ParameterType) is not null;
			if (parameter.ParameterType.IsInstanceOfType(value)) return true;
			var target = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
			return target.IsEnum ? value is string || value.GetType().IsEnum : value is IConvertible && typeof(IConvertible).IsAssignableFrom(target);
		}

		private static object? InvokeStaticAwait(string typeName, string methodName, int timeoutMs, params object?[] arguments)
		{
			return Task.Run(() => AwaitValue(InvokeStaticCompatible(typeName, methodName, arguments), timeoutMs)).GetAwaiter().GetResult();
		}

		private static object? AwaitValue(object? value, int timeoutMs)
		{
			if (value is not Task task)
			{
				var asTask = value?.GetType().GetMethod("AsTask", BindingFlags.Public | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null)?.Invoke(value, null);
				if (asTask is Task valueTask) task = valueTask; else return value;
			}
			if (!task.Wait(timeoutMs)) return null;
			return task.GetType().GetProperty("Result", BindingFlags.Public | BindingFlags.Instance)?.GetValue(task);
		}

		private static string HotloadRevision(object? target)
		{
			var type = target?.GetType();
			var parts = new List<string> { type?.FullName ?? "null", ReadText(target, "Name", "NeedsSwap") ?? "" };
			foreach (var property in new[] { "NeedsSwap", "IsBuilding", "Success", "Errors", "Warnings" }) parts.Add($"{property}={EditorRuntime.GetMember(target, property)}");
			return string.Join("|", parts);
		}

		private static bool IsUnder(string path, string root)
		{
			var fullPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
			var fullRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
			return fullPath.Equals(fullRoot, StringComparison.OrdinalIgnoreCase) || fullPath.StartsWith(fullRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
		}
	}
}