using System;
using System.Collections.Generic;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

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

/// <summary>
/// Thread-safe queue for MxAccess events with capacity overflow and fault tracking.
/// </summary>
public sealed class MxAccessEventQueue
{
    /// <summary>
    /// Default queue capacity (10,000 events).
    /// </summary>
    public const int DefaultCapacity = 10000;

    private readonly int capacity;
    private readonly Queue<WorkerEvent> events;
    private readonly object syncRoot = new();
    private ulong lastEventSequence;
    private WorkerFault? fault;
    private bool faultDrained;

    /// <summary>
    /// Initializes the queue with the default capacity.
    /// </summary>
    public MxAccessEventQueue()
        : this(DefaultCapacity)
    {
    }

    /// <summary>
    /// Initializes the queue with the specified capacity.
    /// </summary>
    /// <param name="capacity">Maximum number of events the queue can hold.</param>
    public MxAccessEventQueue(int capacity)
    {
        if (capacity <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(capacity),
                "MXAccess event queue capacity must be greater than zero.");
        }

        this.capacity = capacity;
        events = new Queue<WorkerEvent>(capacity);
    }

    /// <summary>
    /// The queue's maximum capacity.
    /// </summary>
    public int Capacity => capacity;

    /// <summary>
    /// The current number of events in the queue.
    /// </summary>
    public int Count
    {
        get
        {
            lock (syncRoot)
            {
                return events.Count;
            }
        }
    }

    /// <summary>
    /// The highest event sequence number assigned.
    /// </summary>
    public ulong LastEventSequence
    {
        get
        {
            lock (syncRoot)
            {
                return lastEventSequence;
            }
        }
    }

    /// <summary>
    /// Indicates whether the queue is in a faulted state.
    /// </summary>
    public bool IsFaulted
    {
        get
        {
            lock (syncRoot)
            {
                return fault is not null;
            }
        }
    }

    /// <summary>
    /// The current fault if the queue is faulted, or null.
    /// </summary>
    public WorkerFault? Fault
    {
        get
        {
            lock (syncRoot)
            {
                return fault?.Clone();
            }
        }
    }

    /// <summary>
    /// Enqueues an MxAccess event, assigning a sequence number and timestamp.
    /// </summary>
    /// <param name="mxEvent">MXAccess event to enqueue.</param>
    public void Enqueue(MxEvent mxEvent)
    {
        if (mxEvent is null)
        {
            throw new ArgumentNullException(nameof(mxEvent));
        }

        lock (syncRoot)
        {
            if (fault is not null)
            {
                throw new InvalidOperationException("MXAccess outbound event queue is faulted.");
            }

            if (events.Count >= capacity)
            {
                fault = CreateOverflowFault();
                throw new MxAccessEventQueueOverflowException(capacity);
            }

            MxEvent queuedEvent = mxEvent.Clone();
            queuedEvent.WorkerSequence = ++lastEventSequence;
            queuedEvent.WorkerTimestamp = Timestamp.FromDateTime(DateTime.UtcNow);

            WorkerEvent workerEvent = new()
            {
                Event = queuedEvent,
            };
            events.Enqueue(workerEvent);
        }
    }

    /// <summary>
    /// Attempts to dequeue the next event without removing it if empty.
    /// </summary>
    /// <param name="workerEvent">The dequeued event if successful; null if queue is empty.</param>
    public bool TryDequeue(out WorkerEvent? workerEvent)
    {
        lock (syncRoot)
        {
            if (events.Count == 0)
            {
                workerEvent = null;
                return false;
            }

            workerEvent = events.Dequeue();
            return true;
        }
    }

    /// <summary>
    /// Drains up to maxEvents from the queue; if maxEvents is 0, drains all events.
    /// </summary>
    /// <param name="maxEvents">Maximum number of events to drain; 0 means drain all.</param>
    public IReadOnlyList<WorkerEvent> Drain(uint maxEvents)
    {
        lock (syncRoot)
        {
            int drainCount = maxEvents == 0
                ? events.Count
                : Math.Min(events.Count, checked((int)Math.Min(maxEvents, int.MaxValue)));
            if (drainCount == 0)
            {
                return Array.Empty<WorkerEvent>();
            }

            List<WorkerEvent> drained = new(drainCount);
            for (int index = 0; index < drainCount; index++)
            {
                drained.Add(events.Dequeue());
            }

            return drained;
        }
    }

    /// <summary>
    /// Records a fault if one has not already been recorded.
    /// </summary>
    /// <param name="workerFault">Worker fault to record.</param>
    public void RecordFault(WorkerFault workerFault)
    {
        if (workerFault is null)
        {
            throw new ArgumentNullException(nameof(workerFault));
        }

        lock (syncRoot)
        {
            fault ??= workerFault.Clone();
        }
    }

    /// <summary>
    /// Returns and clears the fault so it is not reported twice.
    /// </summary>
    public WorkerFault? DrainFault()
    {
        lock (syncRoot)
        {
            if (fault is null || faultDrained)
            {
                return null;
            }

            faultDrained = true;
            return fault.Clone();
        }
    }

    private WorkerFault CreateOverflowFault()
    {
        string message = $"MXAccess outbound event queue reached capacity {capacity}.";
        return new WorkerFault
        {
            Category = WorkerFaultCategory.QueueOverflow,
            DiagnosticMessage = message,
            ProtocolStatus = new ProtocolStatus
            {
                Code = ProtocolStatusCode.WorkerUnavailable,
                Message = message,
            },
        };
    }
}
