Editor/OwnedLocalInstance.cs
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Editor.Mcp;

internal sealed class OwnedLocalInstance : IDisposable
{
	/// <summary>
	/// A retained launch may temporarily lack a creation identity after cleanup fails. Such an entry
	/// remains internal until GetProcessTimes supplies the exact timestamp required by the public contract.
	/// </summary>

	internal OwnedLocalInstance( SafeKernelHandle processHandle, SafeKernelHandle jobHandle, int processId,
		long? creationFileTime, string executablePath, bool windowed, int instanceNumber, string logDirectory,
		bool assignedToJob, LaunchAuthority authority )
	{
		if ( creationFileTime is null && authority != LaunchAuthority.RetainedUnidentified )
			throw new ArgumentException( "Only a retained unidentified launch may have no creation identity.",
				nameof( creationFileTime ) );

		ProcessHandle = processHandle;
		JobHandle = jobHandle;
		ProcessId = processId;
		CreationFileTime = creationFileTime;
		ExecutablePath = executablePath;
		Windowed = windowed;
		InstanceNumber = instanceNumber;
		LogDirectory = logDirectory;
		AssignedToJob = assignedToJob;
		Authority = authority;

		LaunchedAt = creationFileTime is long creation
			? DateTime.FromFileTimeUtc( creation ).ToString( "O", CultureInfo.InvariantCulture )
			: null;
	}

	internal SafeKernelHandle ProcessHandle { get; }
	internal SafeKernelHandle JobHandle { get; }
	internal int ProcessId { get; }

	/// <summary>Exact creation FILETIME captured at launch, or null when it could not be read.</summary>
	internal long? CreationFileTime { get; set; }
	internal string ExecutablePath { get; }
	internal string LaunchedAt { get; set; }
	internal bool Windowed { get; }
	internal int InstanceNumber { get; }
	internal string LogDirectory { get; }

	/// <summary>Whether the retained private job actually contains this process.</summary>
	internal bool AssignedToJob { get; }

	/// <summary>
	/// The authority this entry actually holds. A launch that never resumed and whose cleanup failed
	/// is retained in one of the failed-launch states instead of being abandoned, and is never
	/// presented as an ordinary launched process.
	/// </summary>
	internal LaunchAuthority Authority { get; private set; }

	/// <summary>
	/// A launch that was never resumed, so its only authority is the original process handle or its
	/// private job - never a PID, and never a normal launch identity.
	/// </summary>
	internal bool FailedLaunch => Authority != LaunchAuthority.Owned;

	/// <summary>
	/// Marks a never-resumed launch whose pre-resume cleanup failed as the retained authority of that
	/// process. Called by the launch scope while the entry is still unreachable to any other caller.
	/// </summary>
	internal void MarkRetainedFailedLaunch() => Authority = LaunchAuthority.RetainedFailedLaunch;

	/// <summary>Promotes hidden cleanup authority once its exact process creation identity is readable.</summary>
	internal void CaptureCreationIdentity( long creationFileTime )
	{
		if ( CreationFileTime is not null ) return;
		CreationFileTime = creationFileTime;
		LaunchedAt = DateTime.FromFileTimeUtc( creationFileTime ).ToString( "O", CultureInfo.InvariantCulture );
		Authority = LaunchAuthority.RetainedFailedLaunch;
	}


	public void Dispose()
	{
		ProcessHandle.Dispose();
		JobHandle?.Dispose();
	}
}

internal sealed class OwnedInstanceRegistry : IDisposable
{
	internal const ulong SyntheticSteamIdentityBase = 90071996842377216UL;

	private static readonly TimeSpan GracefulBudget = TimeSpan.FromSeconds( 2 );
	private static readonly TimeSpan ForcedBudget = TimeSpan.FromSeconds( 3 );
	private const int PollIntervalMilliseconds = 50;

	private readonly object _sync = new();
	private readonly Dictionary<int, OwnedLocalInstance> _entries = new();

	/// <summary>
	/// Retained launches whose PID key is already held by another entry. A live process can only
	/// collide with an entry whose root has already exited, and dropping either authority would
	/// abandon a live process or its tree, so these stay reachable by their exact (PID, token) pair.
	/// </summary>
	private readonly Dictionary<(int ProcessId, string Token), OwnedLocalInstance> _displaced = new();

	private bool _disposed;

	/// <summary>
	/// Fault seam for the focused harness: fires after a graceful poll delay completes and before
	/// the following cancellation check. Inert unless a test assigns it.
	/// </summary>
	internal Action GracefulDelayCompletedForTesting { get; set; }

	internal OwnedInstanceSnapshot LaunchSbox( bool windowed, Func<ulong, bool> visibleSyntheticIdentity )
	{
		if ( !OperatingSystem.IsWindows() )
			throw new OwnedInstanceException( "Local instance launching is supported only on Windows." );

		var currentExecutable = Environment.ProcessPath;
		if ( string.IsNullOrWhiteSpace( currentExecutable )
			|| !string.Equals( Path.GetFileName( currentExecutable ), "sbox-dev.exe", StringComparison.OrdinalIgnoreCase ) )
			throw new OwnedInstanceException( "Local instances can only be launched by sbox-dev.exe." );

		var directory = Path.GetDirectoryName( currentExecutable );
		var executable = directory is null ? null : Path.Combine( directory, "sbox.exe" );
		if ( executable is null || !File.Exists( executable ) )
			throw new OwnedInstanceException( "The sibling sbox.exe executable was not found." );

		var instanceNumber = AllocateInstanceNumber( visibleSyntheticIdentity );
		var arguments = new List<string> { "-joinlocal", "+instanceid", instanceNumber.ToString( CultureInfo.InvariantCulture ) };
		if ( windowed )
		{
			arguments.Add( "-sw" );
			arguments.Add( "-720" );
		}

		return LaunchContained( executable, arguments, directory, windowed, instanceNumber,
			ResolveLogDirectory(), LaunchFailureInjection.None );
	}

	internal OwnedInstanceSnapshot LaunchContainedForTesting( string executable, IReadOnlyList<string> arguments,
		string workingDirectory, bool windowed = false, int instanceNumber = 1,
		LaunchFailureInjection failure = LaunchFailureInjection.None )
	{
		return LaunchContained( Path.GetFullPath( executable ), arguments, Path.GetFullPath( workingDirectory ),
			windowed, instanceNumber, Path.GetFullPath( workingDirectory ), failure );
	}

	internal OwnedInstanceSnapshot[] List( bool pruneExited )
	{
		lock ( _sync )
		{
			ThrowIfDisposed();
			var rows = new List<(OwnedLocalInstance Entry, OwnedInstanceSnapshot Snapshot)>();
			foreach ( var entry in AllEntries().ToArray() )
			{
				if ( entry.CreationFileTime is null && !TryCaptureCreationIdentity( entry ) )
				{
					if ( pruneExited && IsSignaled( entry.ProcessHandle ) && ActiveProcessCount( entry ) == 0 )
						RemoveAndDispose( entry );
					continue;
				}

				rows.Add( (entry, Snapshot( entry )) );
			}

			if ( pruneExited )
			{
				foreach ( var row in rows )
				{
					if ( row.Snapshot.Exited && row.Snapshot.ActiveProcessCount == 0 )
						RemoveAndDispose( row.Entry );
				}
			}

			return rows.Select( row => row.Snapshot ).ToArray();
		}
	}

	internal bool TryGet( int processId, out OwnedLocalInstance instance )
	{
		lock ( _sync )
		{
			if ( _disposed )
			{
				instance = null;
				return false;
			}

			return _entries.TryGetValue( processId, out instance );
		}
	}

	internal async Task<OwnedTerminationOutcome> TerminateAsync( int processId, string launchTimestamp, bool abrupt,
		CancellationToken cancellationToken )
	{
		if ( processId <= 0 || processId == Environment.ProcessId )
			return OwnedTerminationOutcome.NotOwned( processId );

		OwnedLocalInstance entry;
		lock ( _sync )
		{
			ThrowIfDisposed();
			entry = FindOwned( processId, launchTimestamp );
			if ( entry is null )
				return AnyEntryFor( processId )
					? OwnedTerminationOutcome.IdentityMismatch( processId )
					: OwnedTerminationOutcome.NotOwned( processId );
		}

		cancellationToken.ThrowIfCancellationRequested();
		if ( !ValidateIdentity( entry, out _ ) )
			return OwnedTerminationOutcome.IdentityMismatch( processId );

		if ( IsSignaled( entry.ProcessHandle ) && ActiveProcessCount( entry ) == 0 )
		{
			RemoveAndDispose( entry );
			return OwnedTerminationOutcome.AlreadyExited( processId );
		}

		// A failed launch was never resumed: there is no window to close and nothing to wait for,
		// so it goes straight to the retained authority below.
		if ( !abrupt && !entry.FailedLaunch )
		{
			cancellationToken.ThrowIfCancellationRequested();
			TryCloseMainWindow( entry, IsSignaled( entry.ProcessHandle ) );

			var gracefulDeadline = Stopwatch.StartNew();
			while ( true )
			{
				// Honoured after every await, including the boundary where a poll delay completed
				// and its continuation was queued just as the lifetime was invalidated.
				cancellationToken.ThrowIfCancellationRequested();

				if ( ActiveProcessCount( entry ) == 0 )
				{
					RemoveAndDispose( entry );
					return OwnedTerminationOutcome.Terminated( processId, false, "The owned process job exited after the graceful close request." );
				}

				if ( gracefulDeadline.Elapsed >= GracefulBudget ) break;
				await Task.Delay( PollIntervalMilliseconds, cancellationToken );
				GracefulDelayCompletedForTesting?.Invoke();
			}

			// The last delay may have completed with the budget already spent; nothing may be
			// signaled until ownership is revalidated against a still-valid operation.
			cancellationToken.ThrowIfCancellationRequested();
		}

		if ( !ValidateIdentity( entry, out _ ) )
			return OwnedTerminationOutcome.IdentityMismatch( processId );

		cancellationToken.ThrowIfCancellationRequested();
		var (forced, forcedMessage) = TerminateOwnedTree( entry, processId );

		var forcedDeadline = Stopwatch.StartNew();
		while ( true )
		{
			cancellationToken.ThrowIfCancellationRequested();

			if ( ActiveProcessCount( entry ) == 0 && IsSignaled( entry.ProcessHandle ) )
			{
				RemoveAndDispose( entry );
				return OwnedTerminationOutcome.Terminated( processId, forced, forcedMessage );
			}

			if ( forcedDeadline.Elapsed >= ForcedBudget ) break;
			await Task.Delay( PollIntervalMilliseconds, cancellationToken );
		}

		throw new OwnedInstanceException( "Owned instance termination timed out.", processId );
	}



	internal void CorruptCreationIdentityForTesting( int processId )
	{
		lock ( _sync )
		{
			if ( _entries.TryGetValue( processId, out var entry ) && entry.CreationFileTime is long creation )
				entry.CreationFileTime = creation + 1;
		}
	}

	internal string ReplaceLaunchTimestampForTesting( int processId, string replacement )
	{
		lock ( _sync )
		{
			var entry = _entries[processId];
			var old = entry.LaunchedAt;
			entry.LaunchedAt = replacement;
			return old;
		}
	}

	public void Dispose()
	{
		OwnedLocalInstance[] entries;
		lock ( _sync )
		{
			if ( _disposed ) return;
			_disposed = true;
			entries = AllEntries().ToArray();
			_entries.Clear();
			_displaced.Clear();
		}

		foreach ( var entry in entries ) entry.Dispose();
	}

	private int AllocateInstanceNumber( Func<ulong, bool> visibleSyntheticIdentity )
	{
		for ( var attempt = 0; attempt < 256; attempt++ )
		{
			var candidate = RandomNumberGenerator.GetInt32( 1_000_000, int.MaxValue );
			lock ( _sync )
			{
				ThrowIfDisposed();
				if ( AllEntries().Any( x => x.InstanceNumber == candidate ) ) continue;
			}

			if ( visibleSyntheticIdentity?.Invoke( SyntheticSteamIdentityBase + (ulong)candidate ) == true ) continue;
			return candidate;
		}

		throw new OwnedInstanceException( "Could not allocate a unique local instance identity." );
	}

	private OwnedInstanceSnapshot LaunchContained( string executable, IReadOnlyList<string> arguments,
		string workingDirectory, bool windowed, int instanceNumber, string logDirectory,
		LaunchFailureInjection failure )
	{
		if ( !OperatingSystem.IsWindows() )
			throw new OwnedInstanceException( "Contained process launching is supported only on Windows." );
		if ( !File.Exists( executable ) )
			throw new OwnedInstanceException( "The local instance executable was not found." );

		SafeKernelHandle jobHandle = null;
		SafeKernelHandle processHandle = null;
		IntPtr threadHandle = IntPtr.Zero;
		OwnedLocalInstance entry = null;
		var processId = 0;
		var creationFileTime = 0L;
		var resolvedExecutable = (string)null;
		var assignedToJob = false;
		var resumed = false;

		try
		{
			jobHandle = NativeMethods.CreateJobObjectW( IntPtr.Zero, null );
			if ( jobHandle.IsInvalid )
				throw NativeFailure( "Could not create the private process job." );

			var startup = new NativeMethods.StartupInfo { Size = Marshal.SizeOf<NativeMethods.StartupInfo>() };
			var commandLine = new StringBuilder( BuildCommandLine( executable, arguments ) );
			if ( !NativeMethods.CreateProcessW( executable, commandLine, IntPtr.Zero, IntPtr.Zero, false,
				NativeMethods.CreateSuspended | NativeMethods.CreateNoWindow, IntPtr.Zero, workingDirectory,
				ref startup, out var processInfo ) )
				throw NativeFailure( "Could not create the suspended local instance." );

			processHandle = new SafeKernelHandle( processInfo.ProcessHandle, true );
			threadHandle = processInfo.ThreadHandle;
			processId = checked((int)processInfo.ProcessId);

			// Captured first and unconditionally, so every later failure path - including a failed
			// cleanup - either holds the exact creation-time token this process can be terminated by
			// or knows that none was ever captured and must not be invented.
			creationFileTime = failure.HasFlag( LaunchFailureInjection.CreationTimeCaptureFails )
				? throw new OwnedInstanceException( "Could not read the owned process creation identity.", processId, 5 )
				: GetCreationFileTime( processHandle, processId );

			if ( failure.HasFlag( LaunchFailureInjection.BeforeAssignment ) )
				throw new OwnedInstanceException( "Injected pre-assignment launch failure.", processId );

			var assigned = failure.HasFlag( LaunchFailureInjection.AssignmentFails )
				? false
				: NativeMethods.AssignProcessToJobObject( jobHandle, processHandle );
			if ( !assigned )
				throw new OwnedInstanceException( "Could not contain the local instance in its private process job.",
					processId, failure.HasFlag( LaunchFailureInjection.AssignmentFails ) ? 5 : Marshal.GetLastPInvokeError() );
			assignedToJob = true;

			if ( !NativeMethods.IsProcessInJob( processHandle, jobHandle, out var inJob ) || !inJob )
				throw NativeFailure( "Could not verify private process-job containment.", processId );

			if ( failure.HasFlag( LaunchFailureInjection.AfterAssignmentBeforeIdentity ) )
				throw new OwnedInstanceException( "Injected pre-identity launch failure.", processId );

			if ( failure.HasFlag( LaunchFailureInjection.ImagePathCaptureFails ) )
				throw new OwnedInstanceException( "Could not read the owned process image identity.", processId, 31 );
			resolvedExecutable = QueryImagePath( processHandle );

			var expectedExecutable = Path.GetFullPath( executable );
			if ( !string.Equals( NormalizePath( resolvedExecutable ), NormalizePath( expectedExecutable ), StringComparison.OrdinalIgnoreCase ) )
				throw new OwnedInstanceException( "The launched process image did not match the requested executable.", processId );

			entry = new OwnedLocalInstance( processHandle, jobHandle, processId, creationFileTime,
				resolvedExecutable, windowed, instanceNumber, Path.GetFullPath( logDirectory ), true,
				LaunchAuthority.Owned );
			processHandle = null;
			jobHandle = null;

			lock ( _sync )
			{
				ThrowIfDisposed();
				if ( _entries.ContainsKey( processId ) )
					throw new OwnedInstanceException( "The new process ID collided with an owned registry entry.", processId );
				_entries.Add( processId, entry );
			}

			if ( NativeMethods.ResumeThread( threadHandle ) == uint.MaxValue )
				throw NativeFailure( "Could not resume the contained local instance.", processId );
			resumed = true;
			return Snapshot( entry );
		}
		catch ( Exception launchError ) when ( !resumed )
		{
			var cleanup = CleanupSuspendedLaunch( entry, processHandle, jobHandle, processId,
				assignedToJob, creationFileTime, resolvedExecutable, windowed, instanceNumber, logDirectory, failure );
			if ( cleanup.RetainedProcess ) processHandle = null;
			if ( cleanup.RetainedJob ) jobHandle = null;

			if ( cleanup.Error != 0 )
				throw new OwnedInstanceException( "Local instance launch cleanup failed.", processId, cleanup.Error, launchError );
			throw;
		}
		finally
		{
			if ( threadHandle != IntPtr.Zero ) NativeMethods.CloseHandle( threadHandle );
			processHandle?.Dispose();
			jobHandle?.Dispose();
		}
	}

	/// <summary>
	/// Terminates a suspended, never-resumed launch through the authority actually held for it:
	/// its private job when assignment succeeded, otherwise the original process handle. On failure
	/// the available authority is retained in a distinct failed-launch state so it is not abandoned.
	/// </summary>
	private (int Error, bool RetainedProcess, bool RetainedJob) CleanupSuspendedLaunch( OwnedLocalInstance entry,
		SafeKernelHandle processHandle, SafeKernelHandle jobHandle, int processId, bool assignedToJob,
		long creationFileTime, string resolvedExecutable, bool windowed, int instanceNumber, string logDirectory,
		LaunchFailureInjection failure )
	{
		var error = 0;
		var job = entry?.JobHandle ?? jobHandle;
		var process = entry?.ProcessHandle ?? processHandle;
		var useJob = entry?.AssignedToJob ?? assignedToJob;

		if ( failure.HasFlag( LaunchFailureInjection.CleanupTerminationFails ) )
		{
			error = 5;
		}
		else if ( useJob && job is not null && !job.IsInvalid )
		{
			if ( !NativeMethods.TerminateJobObject( job, 1 ) ) error = Marshal.GetLastPInvokeError();
		}
		else if ( process is not null && !process.IsInvalid && !NativeMethods.TerminateProcess( process, 1 ) )
		{
			error = Marshal.GetLastPInvokeError();
		}

		if ( error == 0 && process is not null && !process.IsInvalid )
		{
			var wait = NativeMethods.WaitForSingleObject( process, 3_000 );
			if ( wait != NativeMethods.WaitObject0 )
				error = wait == NativeMethods.WaitFailed ? Marshal.GetLastPInvokeError() : 1460;
		}

		if ( error == 0 )
		{
			if ( entry is not null )
			{
				lock ( _sync )
				{
					if ( _entries.TryGetValue( processId, out var current ) && ReferenceEquals( current, entry ) )
						_entries.Remove( processId );
				}
				entry.Dispose();
			}
			return (0, false, false);
		}

		if ( entry is not null )
		{
			// The entry already holds both handles, so it becomes the retained authority itself.
			entry.MarkRetainedFailedLaunch();
			RetainFailedLaunch( entry );
			return (error, true, true);
		}

		if ( process is null || process.IsInvalid || processId <= 0 )
			return (error, false, false);

		// Retain what is actually held. The image path may be unavailable after a capture failure and
		// the process may never have joined its job; neither is required to keep cleanup authority,
		// and neither is presented as a contained, fully identified launch. A job handle that is not
		// taken into the entry stays with the launch scope, which releases it.
		var takeJob = useJob && job is not null && !job.IsInvalid;
		var candidate = new OwnedLocalInstance( process, takeJob ? job : null, processId,
			creationFileTime == 0 ? null : creationFileTime, resolvedExecutable, windowed, instanceNumber,
			Path.GetFullPath( logDirectory ), takeJob,
			creationFileTime == 0 ? LaunchAuthority.RetainedUnidentified : LaunchAuthority.RetainedFailedLaunch );
		RetainFailedLaunch( candidate, pidKeyOccupied: failure.HasFlag( LaunchFailureInjection.PidKeyOccupied ) );

		return (error, true, takeJob);
	}

	private OwnedInstanceSnapshot Snapshot( OwnedLocalInstance entry )
	{
		var exited = IsSignaled( entry.ProcessHandle );
		return new OwnedInstanceSnapshot( entry.ProcessId, entry.LaunchedAt, entry.Windowed,
			entry.LogDirectory, !exited, exited, checked((int)ActiveProcessCount( entry )) );
	}

	private static (bool Forced, string Message) TerminateOwnedTree( OwnedLocalInstance entry, int processId )
	{
		if ( entry.AssignedToJob )
		{
			if ( !NativeMethods.TerminateJobObject( entry.JobHandle, 1 ) )
				throw new OwnedInstanceException( "Owned instance termination failed.", processId, Marshal.GetLastPInvokeError() );
			return (true, "The owned process job was terminated.");
		}

		// Only a failed launch reaches here: it never joined its private job, so the original
		// suspended process handle is the authority, and it cannot be confused by PID reuse. No job
		// termination was invoked, so this must never be reported as a forced termination.
		if ( !NativeMethods.TerminateProcess( entry.ProcessHandle, 1 ) )
			throw new OwnedInstanceException( "Owned instance termination failed.", processId, Marshal.GetLastPInvokeError() );
		return (false, "The retained failed launch was terminated through its original process handle; no job termination was invoked.");
	}

	private bool ValidateIdentity( OwnedLocalInstance entry, out int nativeError )
	{
		nativeError = 0;
		if ( NativeMethods.GetProcessId( entry.ProcessHandle ) != (uint)entry.ProcessId )
		{
			nativeError = Marshal.GetLastPInvokeError();
			return false;
		}

		try
		{
			// A retained launch that never captured a creation time has nothing to compare against:
			// its authority is the retained handle itself, which the kernel pins to that exact
			// process object, and the job membership check below still applies when it was assigned.
			if ( entry.CreationFileTime is long creation
				&& GetCreationFileTime( entry.ProcessHandle, entry.ProcessId ) != creation )
				return false;

			// Windows no longer provides the image path after a process exits, and a failed launch
			// may never have captured one. Either way the retained handle, exact creation time and
			// job membership continue to pin that same process.
			if ( entry.ExecutablePath is not null && !IsSignaled( entry.ProcessHandle )
				&& !string.Equals( NormalizePath( QueryImagePath( entry.ProcessHandle ) ),
					NormalizePath( entry.ExecutablePath ), StringComparison.OrdinalIgnoreCase ) )
				return false;
		}
		catch ( OwnedInstanceException ex )
		{
			nativeError = ex.NativeError;
			return false;
		}

		// Membership is only meaningful for a process this registry actually assigned.
		if ( !entry.AssignedToJob ) return true;

		if ( !NativeMethods.IsProcessInJob( entry.ProcessHandle, entry.JobHandle, out var inJob ) )
		{
			nativeError = Marshal.GetLastPInvokeError();
			return false;
		}

		return inJob;
	}

	private static void TryCloseMainWindow( OwnedLocalInstance entry, bool rootExited )
	{
		if ( rootExited || entry.CreationFileTime is null ) return;

		// Post WM_CLOSE rather than Process.CloseMainWindow: PostMessage returns without waiting for
		// the child window thread, so an unresponsive client cannot stall the editor's main thread.
		NativeMethods.EnumWindows( (window, _) =>
		{
			NativeMethods.GetWindowThreadProcessId( window, out var processId );
			if ( processId == (uint)entry.ProcessId )
				NativeMethods.PostMessageW( window, NativeMethods.WindowMessageClose, IntPtr.Zero, IntPtr.Zero );
			return true;
		}, IntPtr.Zero );
	}

	/// <summary>Every retained entry, whether it holds its PID key or a displaced (PID, token) pair.</summary>
	private IEnumerable<OwnedLocalInstance> AllEntries() => _entries.Values.Concat( _displaced.Values );

	/// <summary>The entry whose exact (PID, token) pair matches, in either retention map.</summary>
	private OwnedLocalInstance FindOwned( int processId, string launchTimestamp )
	{
		if ( launchTimestamp is null ) return null;
		if ( _entries.TryGetValue( processId, out var primary )
			&& primary.CreationFileTime is not null
			&& string.Equals( primary.LaunchedAt, launchTimestamp, StringComparison.Ordinal ) )
			return primary;

		if ( _displaced.TryGetValue( (processId, launchTimestamp), out var displaced )
			&& displaced.CreationFileTime is not null )
			return displaced;

		return null;
	}

	/// <summary>Whether either retention map still claims this PID, whatever token was supplied.</summary>
	private bool AnyEntryFor( int processId )
	{
		if ( _entries.ContainsKey( processId ) ) return true;
		foreach ( var identity in _displaced.Keys )
		{
			if ( identity.ProcessId == processId ) return true;
		}

		return false;
	}

	/// <summary>
	/// Publishes a launch that never resumed as a retained failed launch. No caller of this method
	/// disposes or closes the candidate's handles: the authority is always published, and a PID key
	/// already held by another entry is never overwritten, because dropping either authority would
	/// abandon a live process or its tree. Such an entry stays reachable by its exact token, which
	/// termination already matches together with the PID. pidKeyOccupied is the harness seam that
	/// asserts another entry already holds that PID key.
	/// </summary>
	private void RetainFailedLaunch( OwnedLocalInstance candidate, bool pidKeyOccupied = false )
	{
		lock ( _sync )
		{
			if ( _entries.TryGetValue( candidate.ProcessId, out var current ) )
			{
				// Already published under its own PID key; nothing to move or duplicate.
				if ( ReferenceEquals( current, candidate ) ) return;
			}
			else if ( !pidKeyOccupied )
			{
				_entries.Add( candidate.ProcessId, candidate );
				return;
			}

			_displaced[(candidate.ProcessId, candidate.LaunchedAt)] = candidate;
		}
	}

	private void RemoveAndDispose( OwnedLocalInstance entry )
	{
		lock ( _sync )
		{
			if ( _entries.TryGetValue( entry.ProcessId, out var current ) && ReferenceEquals( current, entry ) )
				_entries.Remove( entry.ProcessId );
			else if ( !_displaced.Remove( (entry.ProcessId, entry.LaunchedAt) ) )
				return;
		}
		entry.Dispose();
	}

	private static uint ActiveProcessCount( OwnedLocalInstance entry )
	{
		if ( entry.AssignedToJob ) return GetActiveProcessCount( entry.JobHandle );
		return IsSignaled( entry.ProcessHandle ) ? 0u : 1u;
	}

	private bool TryCaptureCreationIdentity( OwnedLocalInstance entry )
	{
		try
		{
			var creation = GetCreationFileTime( entry.ProcessHandle, entry.ProcessId );
			var wasDisplaced = _displaced.Remove( (entry.ProcessId, entry.LaunchedAt) );
			entry.CaptureCreationIdentity( creation );
			if ( wasDisplaced ) _displaced.Add( (entry.ProcessId, entry.LaunchedAt), entry );
			return true;
		}
		catch ( OwnedInstanceException )
		{
			return false;
		}
	}

	private static uint GetActiveProcessCount( SafeKernelHandle jobHandle )
	{
		if ( !NativeMethods.QueryInformationJobObject( jobHandle, NativeMethods.JobObjectBasicAccountingInformation,
			out var accounting, (uint)Marshal.SizeOf<NativeMethods.JobBasicAccountingInformation>(), IntPtr.Zero ) )
			throw NativeFailure( "Could not inspect the owned process job." );
		return accounting.ActiveProcesses;
	}

	private static bool IsSignaled( SafeKernelHandle processHandle )
	{
		var result = NativeMethods.WaitForSingleObject( processHandle, 0 );
		if ( result == NativeMethods.WaitObject0 ) return true;
		if ( result == NativeMethods.WaitTimeout ) return false;
		throw NativeFailure( "Could not inspect the owned process state." );
	}

	private static long GetCreationFileTime( SafeKernelHandle processHandle, int processId = 0 )
	{
		if ( !NativeMethods.GetProcessTimes( processHandle, out var creation, out _, out _, out _ ) )
			throw new OwnedInstanceException( "Could not read the owned process creation identity.", processId,
				Marshal.GetLastPInvokeError() );
		return creation.ToInt64();
	}

	private static string QueryImagePath( SafeKernelHandle processHandle )
	{
		var capacity = 32768u;
		var buffer = new StringBuilder( checked((int)capacity) );
		if ( !NativeMethods.QueryFullProcessImageNameW( processHandle, 0, buffer, ref capacity ) )
			throw NativeFailure( "Could not read the owned process image identity." );
		return Path.GetFullPath( buffer.ToString() );
	}

	private static string NormalizePath( string path ) => Path.TrimEndingDirectorySeparator( Path.GetFullPath( path ));

	private static string ResolveLogDirectory()
	{
		var root = Environment.GetEnvironmentVariable( "FACEPUNCH_ENGINE", EnvironmentVariableTarget.User );
		if ( string.IsNullOrWhiteSpace( root ) ) root = AppContext.BaseDirectory;
		return Path.GetFullPath( Path.Combine( root, "logs" ) );
	}

	private static string BuildCommandLine( string executable, IReadOnlyList<string> arguments )
	{
		var builder = new StringBuilder();
		AppendQuotedArgument( builder, executable );
		foreach ( var argument in arguments )
		{
			builder.Append( ' ' );
			AppendQuotedArgument( builder, argument ?? string.Empty );
		}
		return builder.ToString();
	}

	private static void AppendQuotedArgument( StringBuilder builder, string argument )
	{
		builder.Append( '"' );
		var slashes = 0;
		foreach ( var character in argument )
		{
			if ( character == '\\' )
			{
				slashes++;
				continue;
			}
			if ( character == '"' )
			{
				builder.Append( '\\', slashes * 2 + 1 );
				builder.Append( '"' );
				slashes = 0;
				continue;
			}
			builder.Append( '\\', slashes );
			slashes = 0;
			builder.Append( character );
		}
		builder.Append( '\\', slashes * 2 );
		builder.Append( '"' );
	}

	private static OwnedInstanceException NativeFailure( string message, int processId = 0 )
	{
		var error = Marshal.GetLastPInvokeError();
		return new OwnedInstanceException( message, processId, error, new Win32Exception( error ));
	}

	private void ThrowIfDisposed()
	{
		if ( _disposed ) throw new ObjectDisposedException( nameof(OwnedInstanceRegistry) );
	}
}

/// <summary>
/// The authority a registry entry holds. A launch that never resumed and whose cleanup could not
/// confirm termination is retained in one of the failed-launch states instead of being abandoned.
/// </summary>
internal enum LaunchAuthority
{
	/// <summary>A fully identified launch whose process was resumed.</summary>
	Owned = 0,

	/// <summary>
	/// A never-resumed launch retained after its cleanup failed with its exact creation time known,
	/// so its token is that creation time and job membership still constrains it when assigned.
	/// </summary>
	RetainedFailedLaunch = 1,

	/// <summary>
	/// A never-resumed launch retained after cleanup failed before its creation time could be read.
	/// It remains internal until the retained handle yields the exact creation identity; it is never
	/// exposed with a fabricated or overloaded public timestamp.
	/// </summary>
	RetainedUnidentified = 2
}

/// <summary>Fault seams used by the focused ownership harness. Production always passes None.</summary>
[Flags]
internal enum LaunchFailureInjection
{
	None = 0,
	BeforeAssignment = 1,
	AfterAssignmentBeforeIdentity = 2,
	AssignmentFails = 4,
	ImagePathCaptureFails = 8,
	CleanupTerminationFails = 16,

	/// <summary>Fails the creation-time read, so no creation identity is ever captured.</summary>
	CreationTimeCaptureFails = 32,

	/// <summary>Retains a failed launch as if its PID key were already held by another entry.</summary>
	PidKeyOccupied = 64
}

internal sealed record OwnedInstanceSnapshot( int ProcessId, string LaunchedAt, bool Windowed,
	string LogDirectory, bool Alive, bool Exited, int ActiveProcessCount );

internal sealed record OwnedTerminationOutcome( int ProcessId, string Result, bool Forced, string Message )
{
	internal static OwnedTerminationOutcome Terminated( int processId, bool forced, string message ) =>
		new( processId, "terminated", forced, message );
	internal static OwnedTerminationOutcome AlreadyExited( int processId ) =>
		new( processId, "already exited", false, "The owned process job had already exited." );
	internal static OwnedTerminationOutcome NotOwned( int processId ) =>
		new( processId, "not owned", false, "No current in-memory ownership entry matches that process." );
	internal static OwnedTerminationOutcome IdentityMismatch( int processId ) =>
		new( processId, "identity mismatch", false, "The supplied or retained launch identity did not match; no process was signaled." );
}

internal sealed class OwnedInstanceException : Exception
{
	internal OwnedInstanceException( string message, int processId = 0, int nativeError = 0, Exception inner = null )
		: base( message, inner )
	{
		ProcessId = processId;
		NativeError = nativeError;
	}
	internal int ProcessId { get; }
	internal int NativeError { get; }
}

internal sealed class SafeKernelHandle : SafeHandleZeroOrMinusOneIsInvalid
{
	internal SafeKernelHandle() : base( true ) { }
	internal SafeKernelHandle( IntPtr handle, bool ownsHandle ) : base( ownsHandle ) => SetHandle( handle );
	protected override bool ReleaseHandle() => NativeMethods.CloseHandle( handle );
}

internal static class NativeMethods
{
	internal const uint CreateSuspended = 0x00000004;
	internal const uint CreateNoWindow = 0x08000000;
	internal const uint WaitObject0 = 0x00000000;
	internal const uint WaitTimeout = 0x00000102;
	internal const uint WaitFailed = 0xFFFFFFFF;
	internal const int JobObjectBasicAccountingInformation = 1;
	internal const uint WindowMessageClose = 0x0010;

	[StructLayout( LayoutKind.Sequential, CharSet = CharSet.Unicode )]
	internal struct StartupInfo
	{
		internal int Size;
		internal string Reserved;
		internal string Desktop;
		internal string Title;
		internal uint X;
		internal uint Y;
		internal uint XSize;
		internal uint YSize;
		internal uint XCountChars;
		internal uint YCountChars;
		internal uint FillAttribute;
		internal uint Flags;
		internal ushort ShowWindow;
		internal ushort Reserved2;
		internal IntPtr Reserved2Pointer;
		internal IntPtr StandardInput;
		internal IntPtr StandardOutput;
		internal IntPtr StandardError;
	}

	[StructLayout( LayoutKind.Sequential )]
	internal struct ProcessInformation
	{
		internal IntPtr ProcessHandle;
		internal IntPtr ThreadHandle;
		internal uint ProcessId;
		internal uint ThreadId;
	}

	[StructLayout( LayoutKind.Sequential )]
	internal struct FileTime
	{
		internal uint Low;
		internal uint High;
		internal long ToInt64() => unchecked((long)(((ulong)High << 32) | Low));
	}

	[StructLayout( LayoutKind.Sequential )]
	internal struct JobBasicAccountingInformation
	{
		internal long TotalUserTime;
		internal long TotalKernelTime;
		internal long ThisPeriodTotalUserTime;
		internal long ThisPeriodTotalKernelTime;
		internal uint TotalPageFaultCount;
		internal uint TotalProcesses;
		internal uint ActiveProcesses;
		internal uint TotalTerminatedProcesses;
	}

	internal delegate bool EnumWindowsCallback( IntPtr window, IntPtr state );

	[DllImport( "user32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool EnumWindows( EnumWindowsCallback callback, IntPtr state );

	[DllImport( "user32.dll", SetLastError = true )]
	internal static extern uint GetWindowThreadProcessId( IntPtr window, out uint processId );

	[DllImport( "user32.dll", EntryPoint = "PostMessageW", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool PostMessageW( IntPtr window, uint message, IntPtr wParam, IntPtr lParam );

	[DllImport( "kernel32.dll", EntryPoint = "CreateJobObjectW", CharSet = CharSet.Unicode, SetLastError = true )]
	internal static extern SafeKernelHandle CreateJobObjectW( IntPtr jobAttributes, string name );

	[DllImport( "kernel32.dll", EntryPoint = "CreateProcessW", CharSet = CharSet.Unicode, SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool CreateProcessW( string applicationName, StringBuilder commandLine,
		IntPtr processAttributes, IntPtr threadAttributes, [MarshalAs( UnmanagedType.Bool )] bool inheritHandles,
		uint creationFlags, IntPtr environment, string currentDirectory, ref StartupInfo startupInfo,
		out ProcessInformation processInformation );

	[DllImport( "kernel32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool AssignProcessToJobObject( SafeKernelHandle job, SafeKernelHandle process );

	[DllImport( "kernel32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool IsProcessInJob( SafeKernelHandle process, SafeKernelHandle job,
		[MarshalAs( UnmanagedType.Bool )] out bool result );

	[DllImport( "kernel32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool GetProcessTimes( SafeKernelHandle process, out FileTime creation,
		out FileTime exit, out FileTime kernel, out FileTime user );

	[DllImport( "kernel32.dll", EntryPoint = "QueryFullProcessImageNameW", CharSet = CharSet.Unicode, SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool QueryFullProcessImageNameW( SafeKernelHandle process, uint flags,
		StringBuilder imagePath, ref uint size );

	[DllImport( "kernel32.dll", SetLastError = true )]
	internal static extern uint GetProcessId( SafeKernelHandle process );

	[DllImport( "kernel32.dll", SetLastError = true )]
	internal static extern uint ResumeThread( IntPtr thread );

	[DllImport( "kernel32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool TerminateProcess( SafeKernelHandle process, uint exitCode );

	[DllImport( "kernel32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool TerminateJobObject( SafeKernelHandle job, uint exitCode );

	[DllImport( "kernel32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool QueryInformationJobObject( SafeKernelHandle job, int informationClass,
		out JobBasicAccountingInformation information, uint informationLength, IntPtr returnLength );

	[DllImport( "kernel32.dll", SetLastError = true )]
	internal static extern uint WaitForSingleObject( SafeKernelHandle handle, uint milliseconds );

	[DllImport( "kernel32.dll", SetLastError = true )]
	[return: MarshalAs( UnmanagedType.Bool )]
	internal static extern bool CloseHandle( IntPtr handle );
}