using System.IO.Pipes;

namespace Dada.Supervisor;

internal sealed class SingleInstanceCoordinator : IDisposable
{
    private readonly string pipeName;
    private readonly ManualResetEventSlim? releaseOwner;
    private readonly Thread? ownerThread;
    private bool disposed;

    private SingleInstanceCoordinator(string pipeName, bool isPrimary, ManualResetEventSlim? releaseOwner, Thread? ownerThread)
    {
        this.pipeName = pipeName;
        IsPrimary = isPrimary;
        this.releaseOwner = releaseOwner;
        this.ownerThread = ownerThread;
    }

    internal bool IsPrimary { get; }

    internal static async Task<SingleInstanceCoordinator> TryAcquireAsync(string mutexName, string pipeName)
    {
        var acquired = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        var release = new ManualResetEventSlim(false);
        var owner = new Thread(() =>
        {
            using var mutex = new Mutex(initiallyOwned: false, mutexName);
            var ownsMutex = false;
            try
            {
                ownsMutex = mutex.WaitOne(0);
                acquired.SetResult(ownsMutex);
                if (ownsMutex) release.Wait();
            }
            catch (AbandonedMutexException)
            {
                ownsMutex = true;
                acquired.SetResult(true);
                release.Wait();
            }
            finally
            {
                if (ownsMutex) mutex.ReleaseMutex();
            }
        }) { IsBackground = true, Name = "Dada instance mutex" };
        owner.Start();
        var isPrimary = await acquired.Task;
        if (isPrimary) return new SingleInstanceCoordinator(pipeName, true, release, owner);

        release.Dispose();
        owner.Join();
        using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous);
        try
        {
            await client.ConnectAsync(2000);
            await client.WriteAsync(new byte[] { 1 });
            await client.FlushAsync();
        }
        catch (TimeoutException)
        {
            // The existing process can still be starting; the second process must exit either way.
        }
        return new SingleInstanceCoordinator(pipeName, false, null, null);
    }

    internal async Task<bool> WaitForOpenRequestAsync(TimeSpan timeout)
    {
        if (!IsPrimary) return false;
        using var cancellation = new CancellationTokenSource(timeout);
        await using var server = new NamedPipeServerStream(pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
        try
        {
            await server.WaitForConnectionAsync(cancellation.Token);
            return server.ReadByte() == 1;
        }
        catch (OperationCanceledException)
        {
            return false;
        }
    }

    public void Dispose()
    {
        if (disposed) return;
        disposed = true;
        releaseOwner?.Set();
        ownerThread?.Join(TimeSpan.FromSeconds(2));
        releaseOwner?.Dispose();
    }
}
