using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Google.Protobuf;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Worker.Ipc;

/// <summary>Writes worker frames to a stream with length-prefixed protobuf serialization.</summary>
public sealed class WorkerFrameWriter
{
    private readonly WorkerFrameProtocolOptions _options;
    private readonly SemaphoreSlim _writeLock = new(1, 1);
    private readonly Stream _stream;

    /// <summary>Initializes a new instance of the WorkerFrameWriter class.</summary>
    /// <param name="stream">Stream to write frames to.</param>
    /// <param name="options">Protocol options for frame encoding.</param>
    public WorkerFrameWriter(
        Stream stream,
        WorkerFrameProtocolOptions options)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
        _options = options ?? throw new ArgumentNullException(nameof(options));
    }

    /// <summary>Writes a worker envelope frame to the stream with length prefix.</summary>
    /// <param name="envelope">Worker envelope to write.</param>
    /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
    public async Task WriteAsync(
        WorkerEnvelope envelope,
        CancellationToken cancellationToken = default)
    {
        if (envelope is null)
        {
            throw new ArgumentNullException(nameof(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.");
        }

        // Serialize once into a single buffer that carries the 4-byte
        // length prefix followed by the payload, then issue one stream write.
        // This avoids a second serialization pass (envelope.ToByteArray()
        // would re-run CalculateSize internally), a separate prefix array,
        // and a separate prefix write.
        int frameLength = sizeof(uint) + payloadLength;
        byte[] frame = new byte[frameLength];
        WriteUInt32LittleEndian(frame, (uint)payloadLength);
        envelope.WriteTo(new Span<byte>(frame, sizeof(uint), payloadLength));

        await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await _stream.WriteAsync(frame, 0, frameLength, cancellationToken).ConfigureAwait(false);
            await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _writeLock.Release();
        }
    }

    private static void WriteUInt32LittleEndian(
        byte[] buffer,
        uint value)
    {
        buffer[0] = (byte)value;
        buffer[1] = (byte)(value >> 8);
        buffer[2] = (byte)(value >> 16);
        buffer[3] = (byte)(value >> 24);
    }
}
