using System.Runtime.CompilerServices;
using System.Threading.Channels;
using ZB.MOM.WW.Audit;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;

/// <summary>
/// Drop-oldest in-memory ring buffer used by <see cref="FallbackAuditWriter"/>
/// when the primary SQLite writer is throwing. Capacity is fixed at construction
/// (default 1024). When full, the oldest event is silently dropped to make room
/// for the newest — preserving the most recent picture of activity in the face
/// of an extended SQLite outage — and <see cref="RingBufferOverflowed"/> is
/// raised so a health counter can record the loss.
/// </summary>
/// <remarks>
/// <para>
/// Backed by a <see cref="Channel{T}"/> with
/// <see cref="BoundedChannelFullMode.DropOldest"/>. The channel doesn't natively
/// notify on drop, so this class compares <c>Reader.Count</c> before and after
/// each enqueue: any time we hit capacity and a subsequent enqueue keeps the
/// count at capacity, exactly one event has been dropped.
/// </para>
/// <para>
/// The ring is the absolute-last-resort buffer for the
/// hot-path; it is NOT a substitute for the bounded
/// <see cref="SqliteAuditWriter"/> write queue.
/// </para>
/// </remarks>
public sealed class RingBufferFallback
{
    private readonly Channel<AuditEvent> _channel;
    private readonly int _capacity;

    /// <summary>
    /// Raised once each time a drop-oldest overflow occurs. Hooked by
    /// <see cref="FallbackAuditWriter"/>'s health counter wiring.
    /// </summary>
    public event Action? RingBufferOverflowed;

    /// <summary>Initializes the ring buffer with the specified fixed capacity.</summary>
    /// <param name="capacity">Maximum number of events to buffer; must be greater than zero. Default is 1024.</param>
    public RingBufferFallback(int capacity = 1024)
    {
        if (capacity <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(capacity), "capacity must be > 0.");
        }

        _capacity = capacity;
        _channel = Channel.CreateBounded<AuditEvent>(new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.DropOldest,
            SingleReader = true,
            SingleWriter = false,
        });
    }

    /// <summary>Current event count in the ring (for diagnostics/tests).</summary>
    public int Count => _channel.Reader.Count;

    /// <summary>
    /// Try to enqueue an event. Returns <see langword="true"/> on success (even
    /// when an overflow caused an older event to be dropped); returns
    /// <see langword="false"/> only when the ring has been
    /// <see cref="Complete"/>-d.
    /// </summary>
    /// <param name="evt">The audit event to enqueue.</param>
    /// <returns><see langword="true"/> if enqueued (or enqueued with overflow); <see langword="false"/> when the channel is completed.</returns>
    public bool TryEnqueue(AuditEvent evt)
    {
        ArgumentNullException.ThrowIfNull(evt);

        // DropOldest TryWrite always succeeds unless the channel is completed.
        // Detect overflow by comparing the count before vs. after: if we were
        // already at capacity and remain at capacity, exactly one event was
        // dropped to make room for evt.
        var beforeCount = _channel.Reader.Count;
        if (!_channel.Writer.TryWrite(evt))
        {
            return false;
        }

        if (beforeCount >= _capacity)
        {
            // The new event displaced an existing one.
            RingBufferOverflowed?.Invoke();
        }

        return true;
    }

    /// <summary>
    /// Drain the ring in FIFO order. Yields available events immediately and
    /// then completes when the channel is empty AND <see cref="Complete"/> has
    /// been called. Callers that only want to drain what's currently buffered
    /// must call <see cref="Complete"/> first.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token to abort the async enumeration.</param>
    /// <returns>An async sequence of buffered <see cref="AuditEvent"/> values in FIFO order.</returns>
    public async IAsyncEnumerable<AuditEvent> DrainAsync(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await foreach (var evt in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
        {
            yield return evt;
        }
    }

    /// <summary>
    /// Non-blocking single-item dequeue used by the
    /// <see cref="FallbackAuditWriter"/> recovery path. Returns
    /// <see langword="false"/> when the ring is empty.
    /// </summary>
    /// <param name="evt">When this returns <see langword="true"/>, contains the dequeued event.</param>
    /// <returns><see langword="true"/> if an event was dequeued; <see langword="false"/> if the ring is empty.</returns>
    public bool TryDequeue(out AuditEvent evt) => _channel.Reader.TryRead(out evt!);

    /// <summary>
    /// Mark the ring as no-more-writes. <see cref="DrainAsync"/> will yield the
    /// remaining events and then complete.
    /// </summary>
    public void Complete() => _channel.Writer.TryComplete();
}
