#nullable enable
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;

namespace IGP.UnitySDK
{
    internal sealed class IGPDesktopNamedPipeTransport : IIGPHostSessionTransport
    {
        private const int ConnectTimeoutMs = 5000;
        private const int MaxFramePayloadBytes = 16 * 1024 * 1024;

        private readonly string pipeEndpoint;
        private readonly SemaphoreSlim writeLock = new SemaphoreSlim(1, 1);
        private NamedPipeClientStream? pipe;
        private CancellationTokenSource? lifetimeCts;
        private Task? readLoopTask;
        private bool disconnecting;

        internal IGPDesktopNamedPipeTransport(string pipeEndpoint)
        {
            this.pipeEndpoint = pipeEndpoint ?? string.Empty;
        }

        public bool IsConnected { get; private set; }

        public event Action<byte[]>? PayloadReceived;
        public event Action<Exception>? Failed;
        public event Action<string>? Closed;

        public async Task ConnectAsync(CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(pipeEndpoint))
            {
                throw new IGPSDKException("IGP desktop session endpoint is missing");
            }

            await DisconnectAsync();

            var localPipe = new NamedPipeClientStream(
                ".",
                ExtractPipeName(pipeEndpoint),
                PipeDirection.InOut,
                PipeOptions.Asynchronous);
            var localLifetimeCts = new CancellationTokenSource();
            pipe = localPipe;
            lifetimeCts = localLifetimeCts;

            try
            {
                await Task.Run(() => localPipe.Connect(ConnectTimeoutMs), cancellationToken);
                cancellationToken.ThrowIfCancellationRequested();
                IsConnected = true;
                readLoopTask = ReadLoopAsync(localPipe, localLifetimeCts.Token);
            }
            catch
            {
                await DisconnectAsync();
                throw;
            }
        }

        public async Task SendAsync(byte[] payload, CancellationToken cancellationToken)
        {
            var localPipe = pipe;
            if (!IsConnected || localPipe == null)
            {
                throw new IGPSDKException("IGP desktop session pipe is not connected");
            }

            if (payload == null || payload.Length == 0 || payload.Length > MaxFramePayloadBytes)
            {
                throw new IGPSDKException("Invalid desktop session payload length");
            }

            var frame = EncodeFrame(payload);
            await writeLock.WaitAsync(cancellationToken);
            try
            {
                await localPipe.WriteAsync(frame, 0, frame.Length, cancellationToken);
                await localPipe.FlushAsync(cancellationToken);
            }
            finally
            {
                writeLock.Release();
            }
        }

        public async Task DisconnectAsync()
        {
            disconnecting = true;
            var localPipe = pipe;
            var localLifetimeCts = lifetimeCts;
            var localReadLoopTask = readLoopTask;
            pipe = null;
            lifetimeCts = null;
            readLoopTask = null;
            IsConnected = false;

            try
            {
                localLifetimeCts?.Cancel();
                localPipe?.Dispose();
                if (localReadLoopTask != null)
                {
                    try
                    {
                        await localReadLoopTask;
                    }
                    catch
                    {
                        // Read failures are delivered through the transport events.
                    }
                }
            }
            finally
            {
                localLifetimeCts?.Dispose();
                disconnecting = false;
            }
        }

        public void Dispose()
        {
            _ = DisconnectAsync().ContinueWith(_ =>
            {
                try
                {
                    writeLock.Dispose();
                }
                catch
                {
                    // Ignore dispose failures during shutdown.
                }
            }, TaskScheduler.Default);
        }

        private async Task ReadLoopAsync(Stream stream, CancellationToken cancellationToken)
        {
            try
            {
                while (!cancellationToken.IsCancellationRequested)
                {
                    var payload = await ReadFrameAsync(stream, cancellationToken);
                    if (payload == null)
                    {
                        break;
                    }

                    PayloadReceived?.Invoke(payload);
                }
            }
            catch (OperationCanceledException)
            {
                // Expected when disconnecting.
            }
            catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
            {
                // Expected when disconnecting.
            }
            catch (IOException) when (cancellationToken.IsCancellationRequested)
            {
                // Expected when disconnecting.
            }
            catch (Exception exception)
            {
                Failed?.Invoke(exception);
            }
            finally
            {
                var shouldNotify = IsConnected && !disconnecting;
                IsConnected = false;
                if (shouldNotify)
                {
                    Closed?.Invoke("desktop-session-closed");
                }
            }
        }

        private static string ExtractPipeName(string endpoint)
        {
            const string pipePrefix = @"\\.\pipe\";
            return endpoint.StartsWith(pipePrefix, StringComparison.OrdinalIgnoreCase)
                ? endpoint.Substring(pipePrefix.Length)
                : endpoint;
        }

        private static byte[] EncodeFrame(byte[] payload)
        {
            var frame = new byte[4 + payload.Length];
            var lengthBytes = BitConverter.GetBytes(payload.Length);
            Buffer.BlockCopy(lengthBytes, 0, frame, 0, 4);
            Buffer.BlockCopy(payload, 0, frame, 4, payload.Length);
            return frame;
        }

        private static async Task<byte[]?> ReadFrameAsync(
            Stream stream,
            CancellationToken cancellationToken)
        {
            var header = await ReadExactAsync(stream, 4, cancellationToken, allowEndOfStream: true);
            if (header == null)
            {
                return null;
            }

            var payloadLength = BitConverter.ToInt32(header, 0);
            if (payloadLength <= 0 || payloadLength > MaxFramePayloadBytes)
            {
                throw new IGPSDKException("Invalid desktop session frame length");
            }

            return await ReadExactAsync(stream, payloadLength, cancellationToken, allowEndOfStream: false);
        }

        private static async Task<byte[]?> ReadExactAsync(
            Stream stream,
            int length,
            CancellationToken cancellationToken,
            bool allowEndOfStream)
        {
            var buffer = new byte[length];
            var offset = 0;
            while (offset < length)
            {
                var read = await stream.ReadAsync(buffer, offset, length - offset, cancellationToken);
                if (read <= 0)
                {
                    if (offset == 0 && allowEndOfStream)
                    {
                        return null;
                    }

                    throw new IGPSDKException("Unexpected end of desktop session stream");
                }

                offset += read;
            }

            return buffer;
        }
    }
}
