namespace ZB.MOM.NatsNet.Server;

internal sealed partial class NatsStream
{
    private readonly object _preAcksSync = new();
    private readonly Dictionary<ulong, HashSet<NatsConsumer>> _preAcks = new();
    private bool _inMonitor;
    private long _replicationOutMsgs;
    private long _replicationOutBytes;

    internal bool NoInterestWithSubject(ulong seq, string subject, NatsConsumer? observingConsumer) =>
        !CheckForInterestWithSubject(seq, subject, observingConsumer);

    internal bool CheckForInterest(ulong seq, NatsConsumer? observingConsumer)
    {
        var subject = string.Empty;
        if (PotentialFilteredConsumers() && Store != null)
        {
            var loaded = Store.LoadMsg(seq, new StoreMsg());
            if (loaded == null)
            {
                RegisterPreAck(observingConsumer, seq);
                return true;
            }

            subject = loaded.Subject;
        }

        return CheckForInterestWithSubject(seq, subject, observingConsumer);
    }

    internal bool CheckForInterestWithSubject(ulong seq, string subject, NatsConsumer? observingConsumer)
    {
        _ = subject;
        lock (_consumersSync)
        {
            foreach (var consumer in _consumerList)
            {
                if (ReferenceEquals(consumer, observingConsumer))
                    continue;

                if (!HasPreAck(consumer, seq))
                    return true;
            }
        }

        ClearAllPreAcks(seq);
        return false;
    }

    internal bool HasPreAck(NatsConsumer? consumer, ulong seq)
    {
        if (consumer == null)
            return false;

        lock (_preAcksSync)
        {
            return _preAcks.TryGetValue(seq, out var consumers) && consumers.Contains(consumer);
        }
    }

    internal bool HasAllPreAcks(ulong seq, string subject)
    {
        lock (_preAcksSync)
        {
            if (!_preAcks.TryGetValue(seq, out var consumers) || consumers.Count == 0)
                return false;
        }

        return NoInterestWithSubject(seq, subject, null);
    }

    internal void ClearAllPreAcks(ulong seq)
    {
        lock (_preAcksSync)
        {
            _preAcks.Remove(seq);
        }
    }

    internal void ClearAllPreAcksBelowFloor(ulong floor)
    {
        lock (_preAcksSync)
        {
            var keys = _preAcks.Keys.Where(k => k < floor).ToArray();
            foreach (var key in keys)
                _preAcks.Remove(key);
        }
    }

    internal void RegisterPreAckLock(NatsConsumer? consumer, ulong seq)
    {
        _mu.EnterWriteLock();
        try
        {
            RegisterPreAck(consumer, seq);
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal void RegisterPreAck(NatsConsumer? consumer, ulong seq)
    {
        if (consumer == null)
            return;

        lock (_preAcksSync)
        {
            if (!_preAcks.TryGetValue(seq, out var consumers))
            {
                consumers = [];
                _preAcks[seq] = consumers;
            }

            consumers.Add(consumer);
        }
    }

    internal void ClearPreAck(NatsConsumer? consumer, ulong seq)
    {
        if (consumer == null)
            return;

        lock (_preAcksSync)
        {
            if (!_preAcks.TryGetValue(seq, out var consumers))
                return;

            consumers.Remove(consumer);
            if (consumers.Count == 0)
                _preAcks.Remove(seq);
        }
    }

    internal bool AckMsg(NatsConsumer? consumer, ulong seq)
    {
        if (seq == 0 || Store == null)
            return false;

        if (Config.Retention == RetentionPolicy.LimitsPolicy)
            return false;

        var state = new StreamState();
        Store.FastState(state);
        if (seq > state.LastSeq)
        {
            RegisterPreAck(consumer, seq);
            return true;
        }

        ClearPreAck(consumer, seq);
        if (seq < state.FirstSeq)
            return false;

        if (!NoInterest(seq, null))
            return false;

        if (!IsClustered())
        {
            var (removed, _) = Store.RemoveMsg(seq);
            return removed;
        }

        return true;
    }

    internal (SnapshotResult? Result, Exception? Error) Snapshot(TimeSpan deadline, bool checkMsgs, bool includeConsumers)
    {
        if (Store == null)
            return (null, new InvalidOperationException("store not initialized"));

        return Store.Snapshot(deadline, includeConsumers, checkMsgs);
    }

    internal void CheckForOrphanMsgs()
    {
        if (Store == null)
            return;

        var state = new StreamState();
        Store.FastState(state);
        ClearAllPreAcksBelowFloor(state.FirstSeq);
    }

    internal void CheckConsumerReplication()
    {
        if (Config.Retention != RetentionPolicy.InterestPolicy)
            return;

        lock (_consumersSync)
        {
            foreach (var consumer in _consumerList)
            {
                if (consumer.Config.Replicas == 0)
                    continue;

                if (consumer.Config.Replicas != Config.Replicas)
                    throw new InvalidOperationException("consumer replicas must match stream replicas for interest retention");
            }
        }
    }

    internal bool CheckInMonitor()
    {
        _mu.EnterWriteLock();
        try
        {
            if (_inMonitor)
                return true;

            _inMonitor = true;
            return false;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal void ClearMonitorRunning()
    {
        _mu.EnterWriteLock();
        try
        {
            _inMonitor = false;
            DeleteBatchApplyState();
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    internal bool IsMonitorRunning()
    {
        _mu.EnterReadLock();
        try
        {
            return _inMonitor;
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    internal void TrackReplicationTraffic(IRaftNode node, int size, int replicas)
    {
        if (!node.IsSystemAccount() || replicas <= 1)
            return;

        var additionalMsgs = replicas - 1;
        var additionalBytes = size * (replicas - 1);
        Interlocked.Add(ref _replicationOutMsgs, additionalMsgs);
        Interlocked.Add(ref _replicationOutBytes, additionalBytes);
    }
}
