using MessagePack;

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

/// <summary>
///     Writes length-prefixed, kind-tagged MessagePack frames to a stream. Thread-safe via
///     <see cref="SemaphoreSlim"/>. Byte-identical mirror of the sidecar's FrameWriter.
/// </summary>
public sealed class FrameWriter : IDisposable
{
    private readonly Stream _stream;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly bool _leaveOpen;

    /// <summary>Initializes a new instance of the FrameWriter class.</summary>
    /// <param name="stream">The underlying stream to write frames to.</param>
    /// <param name="leaveOpen">If true, the stream is not disposed when this writer is disposed.</param>
    public FrameWriter(Stream stream, bool leaveOpen = false)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
        _leaveOpen = leaveOpen;
    }

    /// <summary>Writes a length-prefixed, kind-tagged MessagePack frame to the stream.</summary>
    /// <typeparam name="T">The type of the message to serialize.</typeparam>
    /// <param name="kind">The frame message kind tag.</param>
    /// <param name="message">The message object to serialize and write.</param>
    /// <param name="ct">The cancellation token.</param>
    public async Task WriteAsync<T>(MessageKind kind, T message, CancellationToken ct)
    {
        var body = MessagePackSerializer.Serialize(message, cancellationToken: ct);
        if (body.Length > Framing.MaxFrameBodyBytes)
            throw new InvalidOperationException(
                $"Sidecar IPC frame body {body.Length} exceeds {Framing.MaxFrameBodyBytes} byte cap.");

        var lengthPrefix = new byte[Framing.LengthPrefixSize];
        // Big-endian.
        lengthPrefix[0] = (byte)((body.Length >> 24) & 0xFF);
        lengthPrefix[1] = (byte)((body.Length >> 16) & 0xFF);
        lengthPrefix[2] = (byte)((body.Length >> 8)  & 0xFF);
        lengthPrefix[3] = (byte)( body.Length        & 0xFF);

        await _gate.WaitAsync(ct).ConfigureAwait(false);
        try
        {
            await _stream.WriteAsync(lengthPrefix, ct).ConfigureAwait(false);
            _stream.WriteByte((byte)kind);
            await _stream.WriteAsync(body, ct).ConfigureAwait(false);
            await _stream.FlushAsync(ct).ConfigureAwait(false);
        }
        finally { _gate.Release(); }
    }

    /// <summary>Disposes the writer and underlying stream (if not left open).</summary>
    public void Dispose()
    {
        _gate.Dispose();
        if (!_leaveOpen) _stream.Dispose();
    }
}
