using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MessagePack;

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

/// <summary>
///     Writes length-prefixed, kind-tagged MessagePack frames to a stream. Thread-safe via
///     <see cref="SemaphoreSlim"/> so concurrent producers (heartbeat + reply paths) get
///     serialized writes. Mirror of Driver.Galaxy.Shared.FrameWriter; sidecar carries its
///     own copy.
/// </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.</summary>
    /// <param name="stream">The stream to write frames to.</param>
    /// <param name="leaveOpen">Whether to leave the stream open when disposed.</param>
    public FrameWriter(Stream stream, bool leaveOpen = false)
    {
        _stream = stream ?? throw new ArgumentNullException(nameof(stream));
        _leaveOpen = leaveOpen;
    }

    /// <summary>Writes a frame with the specified message kind and serialized message.</summary>
    /// <typeparam name="T">The type of message being written.</typeparam>
    /// <param name="kind">The message kind identifier.</param>
    /// <param name="message">The message 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 — easy to read in hex dumps.
        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, 0, lengthPrefix.Length, ct).ConfigureAwait(false);
            _stream.WriteByte((byte)kind);
            await _stream.WriteAsync(body, 0, body.Length, ct).ConfigureAwait(false);
            await _stream.FlushAsync(ct).ConfigureAwait(false);
        }
        finally { _gate.Release(); }
    }

    /// <summary>Disposes the frame writer and releases resources.</summary>
    public void Dispose()
    {
        _gate.Dispose();
        if (!_leaveOpen) _stream.Dispose();
    }
}
