using System.Buffers.Binary;
using Google.Protobuf;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Server.Workers;

public sealed class WorkerFrameReader
{
    private readonly WorkerFrameProtocolOptions _options;
    private readonly Stream _stream;

    /// <summary>
    /// Initializes a new instance of <see cref="WorkerFrameReader"/>.
    /// </summary>
    /// <param name="stream">Stream to read frames from.</param>
    /// <param name="options">Frame protocol options.</param>
    public WorkerFrameReader(
        Stream stream,
        WorkerFrameProtocolOptions options)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }

    /// <summary>
    /// Reads a worker envelope frame from the stream asynchronously.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Parsed worker envelope.</returns>
    public async ValueTask<WorkerEnvelope> ReadAsync(CancellationToken cancellationToken = default)
    {
        byte[] lengthPrefix = new byte[sizeof(uint)];
        await ReadExactlyOrThrowAsync(lengthPrefix, cancellationToken).ConfigureAwait(false);

        uint payloadLength = BinaryPrimitives.ReadUInt32LittleEndian(lengthPrefix);
        if (payloadLength == 0)
        {
            throw new WorkerFrameProtocolException(
                WorkerFrameProtocolErrorCode.MalformedLength,
                "Worker frame payload length must be greater than zero.");
        }

        if (payloadLength > _options.MaxMessageBytes)
        {
            throw new WorkerFrameProtocolException(
                WorkerFrameProtocolErrorCode.MessageTooLarge,
                $"Worker frame payload length {payloadLength} exceeds the configured maximum of {_options.MaxMessageBytes} bytes.");
        }

        byte[] payload = new byte[payloadLength];
        await ReadExactlyOrThrowAsync(payload, cancellationToken).ConfigureAwait(false);

        WorkerEnvelope envelope;
        try
        {
            envelope = WorkerEnvelope.Parser.ParseFrom(payload);
        }
        catch (InvalidProtocolBufferException exception)
        {
            throw new WorkerFrameProtocolException(
                WorkerFrameProtocolErrorCode.InvalidEnvelope,
                "Worker frame payload is not a valid WorkerEnvelope protobuf message.",
                exception);
        }

        WorkerEnvelopeValidator.Validate(envelope, _options);

        return envelope;
    }

    private async ValueTask ReadExactlyOrThrowAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken)
    {
        try
        {
            await _stream.ReadExactlyAsync(buffer, cancellationToken).ConfigureAwait(false);
        }
        catch (EndOfStreamException exception)
        {
            throw new WorkerFrameProtocolException(
                WorkerFrameProtocolErrorCode.EndOfStream,
                "Worker frame ended before the expected number of bytes were read.",
                exception);
        }
    }
}
