// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");

using System.Text;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

public sealed partial class ClientConnection
{
    private const string MqttPrefix = "$MQTT.";
    private const string ReplyPrefix = "_R_.";
    private const int MsgScratchSize = 1024;
    private const int MaxDenyPermCacheSize = 256;
    private const int MaxPermCacheSize = 128;
    private const int PruneSize = 32;
    private static readonly TimeSpan StallMin = TimeSpan.FromMilliseconds(2);
    private static readonly TimeSpan StallMax = TimeSpan.FromMilliseconds(5);
    private static readonly TimeSpan StallTotal = TimeSpan.FromMilliseconds(10);

    internal Exception? AddShadowSubscriptions(INatsAccount? acc, Subscription sub)
    {
        if (acc is null)
            return new InvalidOperationException("missing account");
        _ = sub;
        return null;
    }

    internal (Subscription? shadow, Exception? err) AddShadowSub(Subscription sub, object? ime)
    {
        _ = ime;
        var copy = new Subscription
        {
            Subject = sub.Subject.ToArray(),
            Queue = sub.Queue?.ToArray(),
            Sid = sub.Sid?.ToArray(),
            Qw = sub.Qw,
            Client = sub.Client,
        };
        return (copy, null);
    }

    internal bool CanSubscribe(string subject, string? queue = null)
    {
        if (Perms is null)
            return true;

        var checkAllow = !((IsMqtt() || Kind != ClientKind.Client) && subject.StartsWith(MqttPrefix, StringComparison.Ordinal));
        var allowed = true;

        if (checkAllow && Perms.Sub.Allow is not null)
        {
            var result = Perms.Sub.Allow.Match(subject);
            allowed = result.PSubs.Count > 0;
            if (!string.IsNullOrEmpty(queue) && result.QSubs.Count > 0)
                allowed = QueueMatches(queue, result.QSubs);

            if (!allowed && Kind == ClientKind.Leaf && SubscriptionIndex.SubjectHasWildcard(subject))
            {
                var reverse = Perms.Sub.Allow.ReverseMatch(subject);
                allowed = reverse.PSubs.Count != 0;
            }
        }

        if (allowed && Perms.Sub.Deny is not null)
        {
            var result = Perms.Sub.Deny.Match(subject);
            allowed = result.PSubs.Count == 0;
            if (!string.IsNullOrEmpty(queue) && result.QSubs.Count > 0)
                allowed = !QueueMatches(queue, result.QSubs);

            if (allowed && MPerms is null && SubscriptionIndex.SubjectHasWildcard(subject) && DArray is not null)
            {
                foreach (var deny in DArray.Keys)
                {
                    if (SubscriptionIndex.SubjectIsSubsetMatch(deny, subject))
                    {
                        LoadMsgDenyFilter();
                        break;
                    }
                }
            }
        }

        return allowed;
    }

    internal static bool QueueMatches(string queue, IReadOnlyList<List<Subscription>> qsubs)
    {
        if (qsubs.Count == 0)
            return true;

        foreach (var qsub in qsubs)
        {
            if (qsub.Count == 0 || qsub[0].Queue is not { Length: > 0 } q)
                continue;

            var qname = Encoding.ASCII.GetString(q);
            if (queue == qname)
                return true;
            if (SubscriptionIndex.SubjectHasWildcard(qname) && SubscriptionIndex.SubjectIsSubsetMatch(queue, qname))
                return true;
        }
        return false;
    }

    internal void Unsubscribe(INatsAccount? acc, Subscription sub, bool force, bool remove)
    {
        if (!force && sub.IsClosed())
            return;

        lock (_mu)
        {
            if (remove && sub.Sid is { Length: > 0 } sid)
                Subs.Remove(Encoding.ASCII.GetString(sid));
            sub.Close();
        }

        _ = acc;
    }

    internal Exception? ProcessUnsub(byte[] arg)
    {
        var args = SplitArg(arg);
        if (args.Count is < 1 or > 2)
            return new FormatException($"processUnsub Parse Error: {Encoding.ASCII.GetString(arg)}");

        var sid = Encoding.ASCII.GetString(args[0]);
        lock (_mu)
        {
            _in.Subs++;
            if (!Subs.TryGetValue(sid, out var sub))
                return null;

            // Max-delivery based deferred unsub is not modeled yet, so unsubscribe immediately.
            Unsubscribe(_account, sub, force: true, remove: true);
        }

        if (Opts.Verbose)
            SendOK();

        return null;
    }

    internal bool CheckDenySub(string subject)
    {
        if (MPerms is null || MPerms.Deny is null)
            return false;

        if (MPerms.DCache.TryGetValue(subject, out var denied))
            return denied;

        var (np, _) = MPerms.Deny.NumInterest(subject);
        denied = np != 0;
        MPerms.DCache[subject] = denied;
        if (MPerms.DCache.Count > MaxDenyPermCacheSize)
            PruneDenyCache();
        return denied;
    }

    internal byte[] MsgHeaderForRouteOrLeaf(byte[] subj, byte[]? reply, RouteTarget rt, INatsAccount? acc)
    {
        var msg = new List<byte>(MsgScratchSize)
        {
            (byte)(rt.Sub?.Client?.Kind == ClientKind.Leaf ? 'L' : 'R'),
            (byte)'M',
            (byte)'S',
            (byte)'G',
            (byte)' ',
        };

        if (acc is not null && rt.Sub?.Client?.Kind == ClientKind.Router)
        {
            msg.AddRange(Encoding.ASCII.GetBytes(acc.Name));
            msg.Add((byte)' ');
        }

        msg.AddRange(subj);
        msg.Add((byte)' ');

        if (rt.Qs.Length > 0)
        {
            if (reply is { Length: > 0 })
            {
                msg.Add((byte)'+');
                msg.Add((byte)' ');
                msg.AddRange(reply);
                msg.Add((byte)' ');
            }
            else
            {
                msg.Add((byte)'|');
                msg.Add((byte)' ');
            }
            msg.AddRange(rt.Qs);
            msg.Add((byte)' ');
        }
        else if (reply is { Length: > 0 })
        {
            msg.AddRange(reply);
            msg.Add((byte)' ');
        }

        var pa = ParseCtx.Pa;
        if (pa.HeaderSize > 0)
        {
            msg.AddRange(pa.HeaderBytes ?? Encoding.ASCII.GetBytes(pa.HeaderSize.ToString()));
            msg.Add((byte)' ');
            msg.AddRange(pa.SizeBytes ?? Encoding.ASCII.GetBytes(pa.Size.ToString()));
        }
        else
        {
            msg.AddRange(pa.SizeBytes ?? Encoding.ASCII.GetBytes(pa.Size.ToString()));
        }

        msg.Add((byte)'\r');
        msg.Add((byte)'\n');
        return msg.ToArray();
    }

    internal byte[] MsgHeader(byte[] subj, byte[]? reply, Subscription sub)
    {
        var pa = ParseCtx.Pa;
        var hasHeader = pa.HeaderSize > 0;
        var msg = new List<byte>(MsgScratchSize);
        if (hasHeader)
            msg.Add((byte)'H');
        msg.Add((byte)'M');
        msg.Add((byte)'S');
        msg.Add((byte)'G');
        msg.Add((byte)' ');
        msg.AddRange(subj);
        msg.Add((byte)' ');

        if (sub.Sid is { Length: > 0 })
        {
            msg.AddRange(sub.Sid);
            msg.Add((byte)' ');
        }

        if (reply is { Length: > 0 })
        {
            msg.AddRange(reply);
            msg.Add((byte)' ');
        }

        if (hasHeader)
        {
            msg.AddRange(pa.HeaderBytes ?? Encoding.ASCII.GetBytes(pa.HeaderSize.ToString()));
            msg.Add((byte)' ');
            msg.AddRange(pa.SizeBytes ?? Encoding.ASCII.GetBytes(pa.Size.ToString()));
        }
        else
        {
            msg.AddRange(pa.SizeBytes ?? Encoding.ASCII.GetBytes(pa.Size.ToString()));
        }

        msg.Add((byte)'\r');
        msg.Add((byte)'\n');
        return msg.ToArray();
    }

    internal void StalledWait(ClientConnection producer)
    {
        if (producer._in.Tst > StallTotal)
            return;

        var ttl = OutPb >= OutMp && OutMp > 0 ? StallMax : StallMin;
        if (producer._in.Tst + ttl > StallTotal)
            ttl = StallTotal - producer._in.Tst;
        if (ttl <= TimeSpan.Zero)
            return;

        var start = DateTime.UtcNow;
        Thread.Sleep(ttl);
        producer._in.Tst += DateTime.UtcNow - start;
    }

    internal bool DeliverMsg(bool prodIsMqtt, Subscription sub, INatsAccount? acc, byte[] subject, byte[] reply, byte[] mh, byte[] msg, bool gwReply)
    {
        _ = acc;
        _ = subject;
        _ = reply;
        _ = gwReply;

        if (sub.IsClosed())
            return false;

        QueueOutbound(mh);
        QueueOutbound(msg);
        if (prodIsMqtt)
            QueueOutbound("\r\n"u8.ToArray());

        AddToPCD(this);
        return true;
    }

    internal void AddToPCD(ClientConnection client)
    {
        Pcd ??= new Dictionary<ClientConnection, bool>();
        if (Pcd.TryAdd(client, true))
            client.OutPb += 0;
    }

    internal void TrackRemoteReply(string subject, string reply)
    {
        _ = subject;
        _rrTracking ??= new RrTracking
        {
            RMap = new Dictionary<string, object>(StringComparer.Ordinal),
            Lrt = TimeSpan.FromSeconds(1),
        };
        _rrTracking.RMap ??= new Dictionary<string, object>(StringComparer.Ordinal);
        _rrTracking.RMap[reply] = new RespEntry { Time = DateTime.UtcNow };
    }

    internal void PruneRemoteTracking()
    {
        lock (_mu)
        {
            if (_rrTracking?.RMap is null || _rrTracking.RMap.Count == 0)
            {
                _rrTracking = null;
                return;
            }

            var now = DateTime.UtcNow;
            var ttl = _rrTracking.Lrt <= TimeSpan.Zero ? TimeSpan.FromSeconds(1) : _rrTracking.Lrt;
            foreach (var key in _rrTracking.RMap.Keys.ToArray())
            {
                if (_rrTracking.RMap[key] is RespEntry re && now - re.Time > ttl)
                    _rrTracking.RMap.Remove(key);
            }

            if (_rrTracking.RMap.Count == 0)
            {
                _rrTracking.Ptmr?.Dispose();
                _rrTracking = null;
            }
        }
    }

    internal void PruneReplyPerms()
    {
        var resp = Perms?.Resp;
        if (resp is null || Replies is null)
            return;

        var maxMsgs = resp.MaxMsgs;
        var ttl = resp.Expires;
        var now = DateTime.UtcNow;

        foreach (var k in Replies.Keys.ToArray())
        {
            var r = Replies[k];
            if ((maxMsgs > 0 && r.N >= maxMsgs) || (ttl > TimeSpan.Zero && now - r.Time > ttl))
                Replies.Remove(k);
        }

        RepliesSincePrune = 0;
        LastReplyPrune = now;
    }

    internal void PruneDenyCache()
    {
        if (MPerms is null)
            return;

        var removed = 0;
        foreach (var subject in MPerms.DCache.Keys.ToArray())
        {
            MPerms.DCache.Remove(subject);
            if (++removed >= PruneSize)
                break;
        }
    }

    internal void PrunePubPermsCache()
    {
        if (Perms is null)
            return;

        for (var i = 0; i < 5; i++)
        {
            if (Interlocked.CompareExchange(ref Perms.PRun, 1, 0) != 0)
                return;

            var removed = 0;
            foreach (var key in Perms.PCache.Keys.ToArray())
            {
                if (Perms.PCache.Remove(key))
                    removed++;
                if (removed > PruneSize && Perms.PCache.Count <= MaxPermCacheSize)
                    break;
            }

            Interlocked.Add(ref Perms.PcsZ, -removed);
            Interlocked.Exchange(ref Perms.PRun, 0);
            if (Perms.PCache.Count <= MaxPermCacheSize)
                return;
        }
    }

    internal bool PubAllowed(string subject) => PubAllowedFullCheck(subject, fullCheck: true, hasLock: false);

    internal bool PubAllowedFullCheck(string subject, bool fullCheck, bool hasLock)
    {
        if (Perms is null || (Perms.Pub.Allow is null && Perms.Pub.Deny is null))
            return true;

        if (Perms.PCache.TryGetValue(subject, out var cached))
            return cached;

        var checkAllow = !((IsMqtt() || Kind != ClientKind.Client) && subject.StartsWith(MqttPrefix, StringComparison.Ordinal));
        var allowed = true;

        if (checkAllow && Perms.Pub.Allow is not null)
        {
            var (np, _) = Perms.Pub.Allow.NumInterest(subject);
            allowed = np != 0;
        }

        if (allowed && Perms.Pub.Deny is not null)
        {
            var (np, _) = Perms.Pub.Deny.NumInterest(subject);
            allowed = np == 0;
        }

        if (!allowed && fullCheck && Perms.Resp is not null && Replies is not null)
        {
            if (hasLock)
            {
                if (Replies.TryGetValue(subject, out var resp))
                {
                    resp.N++;
                    if ((Perms.Resp.MaxMsgs > 0 && resp.N > Perms.Resp.MaxMsgs)
                        || (Perms.Resp.Expires > TimeSpan.Zero && DateTime.UtcNow - resp.Time > Perms.Resp.Expires))
                    {
                        Replies.Remove(subject);
                    }
                    else
                    {
                        Replies[subject] = resp;
                        allowed = true;
                    }
                }
            }
            else
            {
                lock (_mu)
                {
                    if (Replies.TryGetValue(subject, out var resp))
                    {
                        resp.N++;
                        if ((Perms.Resp.MaxMsgs > 0 && resp.N > Perms.Resp.MaxMsgs)
                            || (Perms.Resp.Expires > TimeSpan.Zero && DateTime.UtcNow - resp.Time > Perms.Resp.Expires))
                        {
                            Replies.Remove(subject);
                        }
                        else
                        {
                            Replies[subject] = resp;
                            allowed = true;
                        }
                    }
                }
            }
        }
        else
        {
            Perms.PCache[subject] = allowed;
            if (Interlocked.Increment(ref Perms.PcsZ) > MaxPermCacheSize)
                PrunePubPermsCache();
        }

        return allowed;
    }

    internal static bool IsServiceReply(byte[] reply) =>
        reply.Length > 3 && Encoding.ASCII.GetString(reply, 0, 4) == ReplyPrefix;
}
