Editor/ModelImportBackend.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace Editor.Mcp;

internal interface IModelImportBackend
{
	object RegisterFile( string absolutePath );
	object FindAsset( string assetPath );
	string GetAssetPath( object asset );
	object CreateModel( object sourceAsset, string absoluteModelPath );
	bool ReadIsCompiled( object asset );
	bool ReadIsCompiledAndUpToDate( object asset );
	bool ReadIsCompileFailed( object asset );
	ValueTask ObserveCompilationIfNeededAsync( object asset );
	IReadOnlyList<object> GetReferences( object asset );
	IReadOnlyList<string> GetUnrecognizedReferences( object asset );
	IReadOnlyList<string> GetInputDependencies( object asset );
	IReadOnlyList<string> GetAdditionalContentFiles( object asset );
}

internal sealed class ModelImportBackendPipeline
{
	private readonly IModelImportBackend _backend;
	internal ModelImportBackendPipeline( IModelImportBackend backend ) => _backend = backend;

	internal object RegisterDependency( string absolutePath, string expectedPath )
	{
		try
		{
			var asset = _backend.RegisterFile( absolutePath );
			if ( asset is null )
				throw new ImportContractException( "RegistrationFailed", $"Failed to register '{expectedPath}'." );
			return asset;
		}
		catch ( ImportContractException ) { throw; }
		catch ( Exception ex ) { throw new ImportContractException( "RegistrationFailed", $"Registration failed for '{expectedPath}': {Safe( ex.Message, 700 )}" ); }
	}

	internal object RegisterSource( string absolutePath, string expectedPath )
	{
		var asset = RegisterDependency( absolutePath, expectedPath );
		if ( !SamePath( asset, expectedPath ) )
			throw new ImportContractException( "RegistrationFailed", $"The source was not registered at '{expectedPath}'." );
		return asset;
	}

	internal object CreateModel( object source, string absolutePath, string expectedPath )
	{
		try
		{
			var created = _backend.CreateModel( source, absolutePath );
			var resolved = _backend.FindAsset( expectedPath );
			if ( created is null || resolved is null || !SamePath( resolved, expectedPath ) )
				throw new ImportContractException( "ModelCreationFailed", "The generated model could not be confirmed at its planned asset path." );
			return resolved;
		}
		catch ( ImportContractException ) { throw; }
		catch ( Exception ex ) { throw new ImportContractException( "ModelCreationFailed", $"Model creation failed: {Safe( ex.Message, 700 )}" ); }
	}

	internal async ValueTask<BackendCompilationEvidence> ObserveCompilationAsync( object model )
	{
		var result = new BackendCompilationEvidence();
		result.UnavailableEvidence["OperationCompletion"] = "Installed asset-state APIs do not expose operation completion.";
		result.UnavailableEvidence["WriterShutdown"] = "Installed asset-state APIs do not expose writer shutdown.";
		Exception helperError = null;
		if ( TryRead( () => _backend.ReadIsCompiled( model ), out var initiallyCompiled, result, "IsCompiled" ) && initiallyCompiled == false )
		{
			try { await _backend.ObserveCompilationIfNeededAsync( model ); }
			catch ( Exception ex ) { helperError = ex; }
		}
		TryRead( () => _backend.ReadIsCompiled( model ), out var isCompiled, result, "IsCompiled" );
		TryRead( () => _backend.ReadIsCompiledAndUpToDate( model ), out var isUpToDate, result, "IsCompiledAndUpToDate" );
		TryRead( () => _backend.ReadIsCompileFailed( model ), out var isFailed, result, "IsCompileFailed" );
		result.IsCompiled = isCompiled;
		result.IsCompiledAndUpToDate = isUpToDate;
		result.IsCompileFailed = isFailed;
		var missing = result.UnavailableEvidence.Keys.Count( x => x is "IsCompiled" or "IsCompiledAndUpToDate" or "IsCompileFailed" );
		result.Status = missing == 0 ? "observed" : missing == 3 ? "unavailable" : "partial";
		if ( result.IsCompileFailed == true ) throw new BackendPipelineException( "CompileFailed", "The model compiler reported failure.", result );
		if ( helperError is not null ) throw new BackendPipelineException( "CompilationUnconfirmed", $"Compilation observation failed: {Safe( helperError.Message, 900 )}", result );
		if ( result.Status != "observed" || result.IsCompiled != true || result.IsCompiledAndUpToDate != true || result.IsCompileFailed != false )
			throw new BackendPipelineException( "CompilationUnconfirmed", "Successful compilation could not be confirmed from all installed asset-state properties.", result );
		return result;
	}

	internal BackendDependencyEvidence InspectDependencies( object source, object model )
	{
		var result = new BackendDependencyEvidence();
		result.UnavailableEvidence["ExhaustiveSourceDependencies"] = "Installed APIs do not expose exhaustive source dependencies.";
		result.UnavailableEvidence["OptionalReferenceClassification"] = "Unrecognized references do not expose optionality.";
		result.UnavailableEvidence["SourceMaterialPreservation"] = "The model helper may substitute materials/default.vmat.";
		var pending = new Queue<object>( [source, model] );
		var seen = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
		var failed = false;
		while ( pending.Count > 0 )
		{
			var asset = pending.Dequeue();
			string path;
			try { path = _backend.GetAssetPath( asset ); }
			catch ( Exception ex )
			{
				RecordUnavailable( result, $"Asset#{seen.Count}", "AssetPath", ex );
				failed = true;
				continue;
			}
			if ( string.IsNullOrWhiteSpace( path ) )
			{
				result.UnavailableEvidence[$"AssetPath:Asset#{seen.Count}"] = "The backend returned no asset path.";
				failed = true;
				continue;
			}
			if ( !seen.Add( path ) ) continue;
			var assetFailed = false;
			IReadOnlyList<string> inputs = [], additional = [];
			try
			{
				foreach ( var reference in _backend.GetReferences( asset ) ?? [] )
				{
					pending.Enqueue( reference );
					try
					{
						var referencePath = _backend.GetAssetPath( reference );
						if ( string.IsNullOrWhiteSpace( referencePath ) ) throw new Exception( "The backend returned no referenced asset path." );
						result.References.Add( referencePath );
					}
					catch ( Exception ex ) { RecordUnavailable( result, path, "ReferencePath", ex ); assetFailed = true; }
				}
			}
			catch ( Exception ex ) { RecordUnavailable( result, path, "References", ex ); assetFailed = true; }
			try { result.UnresolvedReferences.AddRange( _backend.GetUnrecognizedReferences( asset ) ?? [] ); }
			catch ( Exception ex ) { RecordUnavailable( result, path, "UnrecognizedReferences", ex ); assetFailed = true; }
			try { inputs = _backend.GetInputDependencies( asset ) ?? []; result.InputDependencies.AddRange( inputs ); }
			catch ( Exception ex ) { RecordUnavailable( result, path, "InputDependencies", ex ); assetFailed = true; }
			try { additional = _backend.GetAdditionalContentFiles( asset ) ?? []; result.AdditionalContentFiles.AddRange( additional ); }
			catch ( Exception ex ) { RecordUnavailable( result, path, "AdditionalContentFiles", ex ); assetFailed = true; }
			if ( assetFailed ) failed = true;
			else result.InspectedAssets.Add( path );
		}
		Normalize( result.InspectedAssets ); Normalize( result.References ); Normalize( result.InputDependencies );
		Normalize( result.AdditionalContentFiles ); Normalize( result.UnresolvedReferences );
		result.Status = failed ? result.InspectedAssets.Count == 0 ? "unavailable" : "partial" : "inspected";
		if ( result.Status != "inspected" ) throw new BackendPipelineException( "DependencyInspectionFailed", "The promised dependency inspection could not be completed.", result );
		if ( result.UnresolvedReferences.Count > 0 ) throw new BackendPipelineException( "UnresolvedDependencies", "One or more reported references could not be resolved; installed APIs do not expose optionality.", result );
		return result;
	}

	private bool SamePath( object asset, string expected ) => string.Equals( _backend.GetAssetPath( asset )?.Replace( '\\', '/' ), expected, StringComparison.OrdinalIgnoreCase );
	private static bool TryRead( Func<bool> read, out bool? value, BackendCompilationEvidence evidence, string key )
	{
		try { value = read(); evidence.UnavailableEvidence.Remove( key ); return true; }
		catch ( Exception ex ) { value = null; evidence.UnavailableEvidence[key] = Safe( ex.Message, 256 ); return false; }
	}
	private static void RecordUnavailable( BackendDependencyEvidence evidence, string asset, string query, Exception ex ) =>
		evidence.UnavailableEvidence[$"{query}:{Safe( asset, 150 )}"] = Safe( ex.Message, 256 );
	private static void Normalize( List<string> paths )
	{
		var values = paths.Where( x => !string.IsNullOrWhiteSpace( x ) ).Select( x => x.Replace( '\\', '/' ) )
			.Distinct( StringComparer.OrdinalIgnoreCase ).OrderBy( x => x, StringComparer.OrdinalIgnoreCase ).ToArray();
		paths.Clear(); paths.AddRange( values );
	}
	private static string Safe( string value, int maximum )
	{
		var text = string.IsNullOrWhiteSpace( value ) ? "The operation failed without a message." : value.Replace( '\r', ' ' ).Replace( '\n', ' ' );
		return text.Length <= maximum ? text : text[..maximum];
	}
}

internal sealed class BackendCompilationEvidence
{
	internal string Status { get; set; } = "not_started";
	internal bool? IsCompiled { get; set; }
	internal bool? IsCompiledAndUpToDate { get; set; }
	internal bool? IsCompileFailed { get; set; }
	internal Dictionary<string, string> UnavailableEvidence { get; } = new( StringComparer.Ordinal );
}

internal sealed class BackendDependencyEvidence
{
	internal string Status { get; set; } = "not_started";
	internal List<string> InspectedAssets { get; } = [];
	internal List<string> References { get; } = [];
	internal List<string> InputDependencies { get; } = [];
	internal List<string> AdditionalContentFiles { get; } = [];
	internal List<string> UnresolvedReferences { get; } = [];
	internal Dictionary<string, string> UnavailableEvidence { get; } = new( StringComparer.Ordinal );
}

internal sealed class BackendPipelineException : ImportContractException
{
	internal object Evidence { get; }
	internal BackendPipelineException( string code, string message, object evidence ) : base( code, message ) => Evidence = evidence;
}

internal sealed class ModelImportRequestGate
{
	private StrongBox<long> _active;
	internal ModelImportRequestGate() : this( new StrongBox<long>( 0 ) ) { }
	internal ModelImportRequestGate( StrongBox<long> active ) => _active = active ?? throw new ArgumentNullException( nameof( active ) );
	private StrongBox<long> Active => _active ??= new StrongBox<long>( 0 );
	internal StrongBox<long> State => Active;
	internal bool IsActive => System.Threading.Volatile.Read( ref Active.Value ) != 0;
	internal Lease TryEnter()
	{
		var state = Active;
		return System.Threading.Interlocked.CompareExchange( ref state.Value, 1, 0 ) == 0 ? new Lease( state ) : null;
	}
	internal void Restore( bool active ) => System.Threading.Volatile.Write( ref Active.Value, active ? 1 : 0 );

	internal sealed class Lease : IDisposable
	{
		private StrongBox<long> _state;
		internal Lease( StrongBox<long> state ) => _state = state;
		public void Dispose()
		{
			var state = System.Threading.Interlocked.Exchange( ref _state, null );
			if ( state is not null ) System.Threading.Volatile.Write( ref state.Value, 0 );
		}
	}
}
internal sealed record ModelImportHotloadSnapshot(
	int Version, bool ActiveRequest, string[] PendingPaths, string[] OwnedDirectories );

internal static class ModelImportHotloadTransfer
{
	internal static ModelImportHotloadSnapshot Read( IReadOnlyDictionary<string, object> state, bool legacyInvocationFinished )
	{
		var pendingValid = state.TryGetValue( "ModelImportPending", out var pendingValue ) && pendingValue is IEnumerable<string>;
		var ownedValid = state.TryGetValue( "ModelImportOwnedDirectories", out var ownedValue ) && ownedValue is IEnumerable<string>;
		var busyValid = state.TryGetValue( "ModelImportBusy", out var busyValue ) && busyValue is bool;
		if ( !pendingValid || !ownedValid || !busyValid )
			throw new ImportContractException( "ImportBusy", "Model import hotload state transfer was incomplete." );
		var version = state.TryGetValue( "ModelImportStateVersion", out var versionValue ) && versionValue is int parsed ? parsed : 1;
		var active = version >= 4 ? (bool)busyValue : !legacyInvocationFinished && (bool)busyValue;
		return new ModelImportHotloadSnapshot( version, active,
			((IEnumerable<string>)pendingValue).Distinct( StringComparer.OrdinalIgnoreCase ).ToArray(),
			((IEnumerable<string>)ownedValue).Distinct( StringComparer.OrdinalIgnoreCase ).ToArray() );
	}

	internal static void Write( Dictionary<string, object> state, ModelImportRequestGate gate, IEnumerable<string> pending, IEnumerable<string> owned )
	{
		state["ModelImportStateVersion"] = 4;
		state["ModelImportBusy"] = gate.IsActive;
		state["ModelImportGateState"] = gate.State;
		state["ModelImportPending"] = pending.Distinct( StringComparer.OrdinalIgnoreCase ).ToArray();
		state["ModelImportOwnedDirectories"] = owned.Distinct( StringComparer.OrdinalIgnoreCase ).ToArray();
	}
}