using MessagePack;

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

/// <summary>
///     Reads length-prefixed, kind-tagged frames from a stream. Single-consumer — do not call
///     <see cref="ReadFrameAsync"/> from multiple threads against the same instance. Mirror of
///     the sidecar's <c>FrameReader</c>; kept byte-identical so the wire protocol stays stable.
/// </summary>
public sealed class FrameReader : IDisposable
{
    private readonly Stream _stream;
    private readonly bool _leaveOpen;

    /// <summary>Initializes a new instance of the <see cref="FrameReader"/> class.</summary>
    /// <param name="stream">The stream to read frames from.</param>
    /// <param name="leaveOpen">True to leave the stream open after disposal; false to dispose it.</param>
    public FrameReader(Stream stream, bool leaveOpen = false)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
        _leaveOpen = leaveOpen;
    }

    /// <summary>Reads a single frame from the stream.</summary>
    /// <param name="ct">A cancellation token.</param>
    /// <returns>A tuple of the message kind and body bytes, or null at end-of-stream.</returns>
    public async Task<(MessageKind Kind, byte[] Body)?> ReadFrameAsync(CancellationToken ct)
    {
        var lengthPrefix = new byte[Framing.LengthPrefixSize];
        if (!await ReadExactAsync(lengthPrefix, ct).ConfigureAwait(false))
            return null; // clean EOF on frame boundary

        var length = (lengthPrefix[0] << 24) | (lengthPrefix[1] << 16) | (lengthPrefix[2] << 8) | lengthPrefix[3];
        if (length < 0 || length > Framing.MaxFrameBodyBytes)
            throw new InvalidDataException($"Sidecar IPC frame length {length} out of range.");

        // Read the kind byte asynchronously and cancellably — a synchronous ReadByte()
        // blocks the thread-pool thread and cannot be interrupted by the call-timeout token
        // if the peer stalls mid-frame (finding 005).
        var kindBuffer = new byte[Framing.KindByteSize];
        if (!await ReadExactAsync(kindBuffer, ct).ConfigureAwait(false))
            throw new EndOfStreamException("EOF after length prefix, before kind byte.");

        var body = new byte[length];
        if (!await ReadExactAsync(body, ct).ConfigureAwait(false))
            throw new EndOfStreamException("EOF mid-frame.");

        return ((MessageKind)kindBuffer[0], body);
    }

    /// <summary>Deserializes a frame body from MessagePack binary format.</summary>
    /// <typeparam name="T">The target type to deserialize the body into.</typeparam>
    /// <param name="body">The frame body bytes to deserialize.</param>
    /// <returns>The deserialized object of the specified type.</returns>
    public static T Deserialize<T>(byte[] body) => MessagePackSerializer.Deserialize<T>(body);

    private async Task<bool> ReadExactAsync(byte[] buffer, CancellationToken ct)
    {
        var offset = 0;
        while (offset < buffer.Length)
        {
            var read = await _stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), ct).ConfigureAwait(false);
            if (read == 0)
            {
                if (offset == 0) return false;
                throw new EndOfStreamException($"Stream ended after reading {offset} of {buffer.Length} bytes.");
            }
            offset += read;
        }
        return true;
    }

    /// <summary>Releases the stream resources if <c>leaveOpen</c> was false.</summary>
    public void Dispose()
    {
        if (!_leaveOpen) _stream.Dispose();
    }
}
