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

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

/// <summary>
/// Writes length-prefixed WorkerEnvelope protobuf messages to a stream.
/// </summary>
public sealed class WorkerFrameWriter
{
    private readonly WorkerFrameProtocolOptions _options;
    private readonly Stream _stream;

    /// <summary>
    /// Initializes the writer with a stream and frame protocol options.
    /// </summary>
    /// <param name="stream">Stream to write frames to.</param>
    /// <param name="options">Frame protocol configuration.</param>
    public WorkerFrameWriter(
        Stream stream,
        WorkerFrameProtocolOptions options)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }

    /// <summary>
    /// Writes a WorkerEnvelope as a length-prefixed message to the stream.
    /// </summary>
    /// <param name="envelope">Worker envelope message to write.</param>
    /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
    public async ValueTask WriteAsync(
        WorkerEnvelope envelope,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(envelope);
        WorkerEnvelopeValidator.Validate(envelope, _options);

        int payloadLength = envelope.CalculateSize();
        if (payloadLength == 0)
        {
            throw new WorkerFrameProtocolException(
                WorkerFrameProtocolErrorCode.InvalidEnvelope,
                "Worker envelope cannot serialize to an empty payload.");
        }

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

        byte[] lengthPrefix = new byte[sizeof(uint)];
        BinaryPrimitives.WriteUInt32LittleEndian(lengthPrefix, (uint)payloadLength);

        await _stream.WriteAsync(lengthPrefix, cancellationToken).ConfigureAwait(false);
        await _stream.WriteAsync(envelope.ToByteArray(), cancellationToken).ConfigureAwait(false);
    }
}
