using System.IO.Pipes;
using MessagePack;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc;

namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Internal;

/// <summary>
///     Owns one connection to the Wonderware historian sidecar pipe. Handles the Hello
///     handshake, serializes outgoing requests + waits for the matching reply frame, and
///     reconnects on transport failure with exponential backoff.
/// </summary>
/// <remarks>
///     Single in-flight call at a time — the sidecar's pipe protocol is request/response
///     over a single bidirectional stream, so multiple concurrent <see cref="InvokeAsync"/>
///     calls would interleave replies. A <see cref="SemaphoreSlim"/> serializes them. PR 6.x
///     can layer batching on top.
/// </remarks>
internal sealed class PipeChannel : IAsyncDisposable
{
    private readonly WonderwareHistorianClientOptions _options;
    private readonly Func<CancellationToken, Task<Stream>> _connect;
    private readonly ILogger _logger;
    private readonly SemaphoreSlim _callGate = new(1, 1);

    private Stream? _stream;
    private FrameReader? _reader;
    private FrameWriter? _writer;
    private bool _disposed;

    /// <summary>
    ///     Default factory: connects to a real <see cref="NamedPipeClientStream"/> by name.
    /// </summary>
    public static Func<WonderwareHistorianClientOptions, CancellationToken, Task<Stream>> DefaultNamedPipeConnectFactory =
        async (opts, ct) =>
        {
            var pipe = new NamedPipeClientStream(
                serverName: ".",
                pipeName: opts.PipeName,
                direction: PipeDirection.InOut,
                options: PipeOptions.Asynchronous);

            await pipe.ConnectAsync((int)opts.EffectiveConnectTimeout.TotalMilliseconds, ct).ConfigureAwait(false);
            return pipe;
        };

    /// <summary>Initializes a new instance of the <see cref="PipeChannel"/> class.</summary>
    /// <param name="options">Configuration options for the historian client.</param>
    /// <param name="connect">Function to establish a connection stream.</param>
    /// <param name="logger">Logger instance for diagnostics.</param>
    public PipeChannel(
        WonderwareHistorianClientOptions options,
        Func<CancellationToken, Task<Stream>> connect,
        ILogger logger)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _connect = connect ?? throw new ArgumentNullException(nameof(connect));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>Gets a value indicating whether the channel is currently connected.</summary>
    public bool IsConnected => _stream is not null;

    /// <summary>
    ///     Connects + performs the Hello handshake. Returns when the sidecar has accepted the
    ///     hello. Throws on rejection (bad secret, version mismatch, or transport failure).
    /// </summary>
    /// <param name="ct">Cancellation token to stop the operation.</param>
    /// <returns>A task representing the asynchronous connection operation.</returns>
    public async Task ConnectAsync(CancellationToken ct)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        await _callGate.WaitAsync(ct).ConfigureAwait(false);
        try
        {
            await ConnectInternalAsync(ct).ConfigureAwait(false);
        }
        finally { _callGate.Release(); }
    }

    /// <summary>
    ///     Sends one request, waits for the matching reply. On transport failure, reconnects
    ///     once and retries — broader retry policy lives in the calling layer.
    /// </summary>
    /// <typeparam name="TRequest">The type of the request payload.</typeparam>
    /// <typeparam name="TReply">The type of the reply payload.</typeparam>
    /// <param name="requestKind">The message kind of the request.</param>
    /// <param name="expectedReplyKind">The expected message kind of the reply.</param>
    /// <param name="request">The request payload to send.</param>
    /// <param name="cancellationToken">Cancellation token to stop the operation.</param>
    /// <returns>A task that returns the reply payload.</returns>
    public async Task<TReply> InvokeAsync<TRequest, TReply>(
        MessageKind requestKind,
        MessageKind expectedReplyKind,
        TRequest request,
        CancellationToken cancellationToken)
        where TReply : class
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(_options.EffectiveCallTimeout);

        await _callGate.WaitAsync(timeout.Token).ConfigureAwait(false);
        try
        {
            // Lazy connect on first call.
            if (_stream is null) await ConnectInternalAsync(timeout.Token).ConfigureAwait(false);

            try
            {
                return await ExchangeAsync<TRequest, TReply>(requestKind, expectedReplyKind, request, timeout.Token).ConfigureAwait(false);
            }
            catch (Exception ex) when (ex is IOException or EndOfStreamException or ObjectDisposedException)
            {
                _logger.LogWarning(ex, "Sidecar pipe transport failure on {Kind}; reconnecting", requestKind);
                ResetTransport();
                await ConnectInternalAsync(timeout.Token).ConfigureAwait(false);
                // One retry. If the second attempt also fails, propagate.
                return await ExchangeAsync<TRequest, TReply>(requestKind, expectedReplyKind, request, timeout.Token).ConfigureAwait(false);
            }
        }
        finally { _callGate.Release(); }
    }

    private async Task<TReply> ExchangeAsync<TRequest, TReply>(
        MessageKind requestKind, MessageKind expectedReplyKind, TRequest request, CancellationToken ct)
    {
        await _writer!.WriteAsync(requestKind, request, ct).ConfigureAwait(false);
        var frame = await _reader!.ReadFrameAsync(ct).ConfigureAwait(false)
            ?? throw new EndOfStreamException("Sidecar closed pipe before reply.");
        if (frame.Kind != expectedReplyKind)
        {
            throw new InvalidDataException(
                $"Sidecar replied with kind {frame.Kind}; expected {expectedReplyKind}.");
        }
        return MessagePackSerializer.Deserialize<TReply>(frame.Body);
    }

    private async Task ConnectInternalAsync(CancellationToken ct)
    {
        ResetTransport();

        _stream = await _connect(ct).ConfigureAwait(false);
        _reader = new FrameReader(_stream, leaveOpen: true);
        _writer = new FrameWriter(_stream, leaveOpen: true);

        var hello = new Hello
        {
            ProtocolMajor = Hello.CurrentMajor,
            ProtocolMinor = Hello.CurrentMinor,
            PeerName = _options.PeerName,
            SharedSecret = _options.SharedSecret,
        };
        await _writer.WriteAsync(MessageKind.Hello, hello, ct).ConfigureAwait(false);

        var ackFrame = await _reader.ReadFrameAsync(ct).ConfigureAwait(false)
            ?? throw new EndOfStreamException("Sidecar closed pipe before HelloAck.");
        if (ackFrame.Kind != MessageKind.HelloAck)
        {
            ResetTransport();
            throw new InvalidDataException($"Sidecar replied to Hello with kind {ackFrame.Kind}; expected HelloAck.");
        }

        var ack = MessagePackSerializer.Deserialize<HelloAck>(ackFrame.Body);
        if (!ack.Accepted)
        {
            ResetTransport();
            throw new UnauthorizedAccessException(
                $"Sidecar rejected Hello: {ack.RejectReason ?? "<no reason>"}.");
        }

        _logger.LogInformation("Sidecar pipe connected — host={Host}", ack.HostName);
    }

    private void ResetTransport()
    {
        _writer?.Dispose();
        _reader?.Dispose();
        _stream?.Dispose();
        _writer = null;
        _reader = null;
        _stream = null;
    }

    /// <summary>Releases all resources associated with this channel.</summary>
    /// <returns>A task representing the asynchronous disposal operation.</returns>
    public ValueTask DisposeAsync()
    {
        if (_disposed) return ValueTask.CompletedTask;
        _disposed = true;
        ResetTransport();
        _callGate.Dispose();
        return ValueTask.CompletedTask;
    }
}
