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

using System.Net.Security;
using System.Text;
using System.Text.Json;
using System.Linq;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Protocol;

namespace ZB.MOM.NatsNet.Server;

public sealed partial class ClientConnection
{
    internal Leaf? Leaf;

    internal bool IsSolicitedLeafNode() => Kind == ClientKind.Leaf && Leaf?.Remote != null;

    internal bool IsSpokeLeafNode() => Kind == ClientKind.Leaf && Leaf?.IsSpoke == true;

    internal bool IsIsolatedLeafNode() => Kind == ClientKind.Leaf && Leaf?.Isolated == true;

    internal Exception? SendLeafConnect(string clusterName, bool headers)
    {
        if (Server is not NatsServer server)
            return new InvalidOperationException("server unavailable for leaf connect");

        lock (_mu)
        {
            Leaf ??= new Leaf();
            Leaf.Remote ??= new LeafNodeCfg();

            var remote = Leaf.Remote;
            var connectInfo = new LeafConnectInfo
            {
                Version = ServerConstants.Version,
                Id = server.ID(),
                Domain = server.GetOpts().JetStreamDomain,
                Name = server.Name(),
                Hub = remote.RemoteOpts?.Hub == true,
                Cluster = clusterName,
                Headers = headers,
                JetStream = false,
                DenyPub = remote.RemoteOpts?.DenyImports?.ToArray() ?? [],
                Compression = string.IsNullOrWhiteSpace(Leaf.Compression) ? CompressionMode.NotSupported : Leaf.Compression,
                RemoteAccount = _account?.Name ?? string.Empty,
                Proto = server.GetServerProto(),
                Isolate = remote.RemoteOpts?.RequestIsolation == true,
            };

            if (remote.CurUrl?.UserInfo is { Length: > 0 } userInfo)
            {
                var userInfoParts = userInfo.Split(':', 2, StringSplitOptions.None);
                connectInfo.User = userInfoParts[0];
                connectInfo.Pass = userInfoParts.Length > 1 ? userInfoParts[1] : string.Empty;
                if (string.IsNullOrEmpty(connectInfo.Pass))
                    connectInfo.Token = connectInfo.User;
            }
            else if (!string.IsNullOrWhiteSpace(remote.Username))
            {
                connectInfo.User = remote.Username;
                connectInfo.Pass = remote.Password;
                if (string.IsNullOrEmpty(connectInfo.Pass))
                    connectInfo.Token = connectInfo.User;
            }

            var payload = JsonSerializer.Serialize(connectInfo);
            EnqueueProto(Encoding.ASCII.GetBytes($"CONNECT {payload}\r\n"));
        }

        return null;
    }

    internal Exception? LeafClientHandshakeIfNeeded(ServerInfo info)
    {
        if (!info.TlsRequired)
            return null;

        Opts.TlsRequired = true;
        return null;
    }

    internal void ProcessLeafnodeInfo(ServerInfo info)
    {
        if (Server is not NatsServer server)
            return;

        lock (_mu)
        {
            Leaf ??= new Leaf();
            if (string.IsNullOrWhiteSpace(Leaf.Compression))
                Leaf.Compression = string.IsNullOrWhiteSpace(info.Compression) ? CompressionMode.NotSupported : info.Compression!;

            Headers = server.SupportsHeaders() && info.Headers;
            Flags |= ClientFlags.InfoReceived;
        }

        if (IsSolicitedLeafNode())
            UpdateLeafNodeURLs(info);
    }

    internal void UpdateLeafNodeURLs(ServerInfo info)
    {
        var cfg = Leaf?.Remote;
        if (cfg is null)
            return;

        cfg.AcquireWriteLock();
        try
        {
            var useWebSocket = cfg.Urls.Count > 0 && string.Equals(cfg.Urls[0].Scheme, "ws", StringComparison.OrdinalIgnoreCase);
            if (useWebSocket)
            {
                var scheme = cfg.RemoteOpts?.Tls == true ? "wss" : "ws";
                DoUpdateLNURLs(cfg, scheme, info.WsConnectUrls ?? []);
            }
            else
            {
                DoUpdateLNURLs(cfg, "nats-leaf", info.LeafNodeUrls ?? []);
            }
        }
        finally
        {
            cfg.ReleaseWriteLock();
        }
    }

    internal void DoUpdateLNURLs(LeafNodeCfg cfg, string scheme, string[] urls)
    {
        var dynamicUrls = new List<Uri>(urls.Length + cfg.Urls.Count);

        foreach (var url in urls)
        {
            if (!Uri.TryCreate($"{scheme}://{url}", UriKind.Absolute, out var parsed))
            {
                Errorf("Error parsing url {0}", url);
                continue;
            }

            var isDuplicate = false;
            foreach (var configured in cfg.Urls)
            {
                if (string.Equals(parsed.Host, configured.Host, StringComparison.OrdinalIgnoreCase)
                    && parsed.Port == configured.Port)
                {
                    isDuplicate = true;
                    break;
                }
            }

            if (isDuplicate)
                continue;

            dynamicUrls.Add(parsed);
            cfg.SaveTLSHostname(parsed);
        }

        dynamicUrls.AddRange(cfg.Urls);
        cfg.Urls = dynamicUrls;
        cfg.CurUrl ??= cfg.Urls.FirstOrDefault();
    }

    internal Exception? ProcessLeafNodeConnect(NatsServer server, byte[] arg, string lang)
    {
        if (!string.IsNullOrWhiteSpace(lang))
            return ServerErrors.ErrClientConnectedToLeafNodePort;

        LeafConnectInfo? protocol;
        try
        {
            protocol = JsonSerializer.Deserialize<LeafConnectInfo>(arg);
        }
        catch (Exception ex)
        {
            return ex;
        }

        if (protocol is null)
            return new InvalidOperationException("invalid leaf connect payload");

        if (!string.IsNullOrWhiteSpace(protocol.Cluster) && protocol.Cluster.Contains(' '))
            return ServerErrors.ErrClusterNameHasSpaces;

        var cachedClusterName = server.CachedClusterName();
        if (!string.IsNullOrWhiteSpace(cachedClusterName)
            && !string.IsNullOrWhiteSpace(protocol.Cluster)
            && string.Equals(cachedClusterName, protocol.Cluster, StringComparison.Ordinal))
            return ServerErrors.ErrLeafNodeHasSameClusterName;

        lock (_mu)
        {
            Leaf ??= new Leaf();
            Opts.Verbose = false;
            Opts.Echo = false;
            Opts.Pedantic = false;
            Headers = server.SupportsHeaders() && protocol.Headers;

            if (string.IsNullOrWhiteSpace(Leaf.Compression))
                Leaf.Compression = string.IsNullOrWhiteSpace(protocol.Compression) ? CompressionMode.NotSupported : protocol.Compression;

            Leaf.RemoteServer = protocol.Name;
            Leaf.RemoteAccName = protocol.RemoteAccount;
            Leaf.Isolated = Leaf.Isolated || protocol.Isolate;
            Leaf.IsSpoke = protocol.Hub;
            Leaf.RemoteCluster = protocol.Cluster;
            Leaf.RemoteDomain = protocol.Domain;

            if (protocol.DenyPub is { Length: > 0 })
                MergeDenyPermissions(DenyType.Pub, protocol.DenyPub);
        }

        server.AddLeafNodeConnection(this, protocol.Name, protocol.Cluster, checkForDup: true);
        server.SendPermsAndAccountInfo(this);
        server.InitLeafNodeSmapAndSendSubs(this);
        server.CheckInternalSyncConsumers(_account as Account);
        return null;
    }

    internal void UpdateSmap(Internal.Subscription sub, int delta, bool isLds)
    {
        _ = isLds;

        lock (_mu)
        {
            if (Leaf?.Smap is null)
                return;

            var key = LeafNodeHandler.KeyFromSub(sub);
            Leaf.Smap.TryGetValue(key, out var current);
            var next = current + delta;

            var isQueue = sub.Queue is { Length: > 0 };
            var shouldUpdate = isQueue || (current <= 0 && next > 0) || (current > 0 && next <= 0);

            if (next > 0)
                Leaf.Smap[key] = next;
            else
                Leaf.Smap.Remove(key);

            if (shouldUpdate)
                SendLeafNodeSubUpdate(key, next);
        }
    }

    internal void ForceAddToSmap(string subject)
    {
        lock (_mu)
        {
            if (Leaf?.Smap is null)
                return;

            if (Leaf.Smap.TryGetValue(subject, out var value) && value != 0)
                return;

            Leaf.Smap[subject] = 1;
            SendLeafNodeSubUpdate(subject, 1);
        }
    }

    internal void ForceRemoveFromSmap(string subject)
    {
        lock (_mu)
        {
            if (Leaf?.Smap is null)
                return;

            if (!Leaf.Smap.TryGetValue(subject, out var value) || value == 0)
                return;

            value--;
            if (value <= 0)
            {
                Leaf.Smap.Remove(subject);
                SendLeafNodeSubUpdate(subject, 0);
            }
            else
            {
                Leaf.Smap[subject] = value;
            }
        }
    }

    internal void SendLeafNodeSubUpdate(string key, int n)
    {
        if (string.IsNullOrWhiteSpace(key))
            return;

        if (IsSpokeLeafNode())
        {
            var subject = key;
            var separator = key.IndexOf(' ');
            if (separator > 0)
                subject = key[..separator];

            var checkPermissions = !subject.StartsWith(LeafNodeHandler.LeafNodeLoopDetectionSubjectPrefix, StringComparison.Ordinal)
                && !subject.StartsWith("$GR.", StringComparison.Ordinal)
                && !subject.StartsWith("$GNR.", StringComparison.Ordinal);
            if (checkPermissions && !CanSubscribe(subject))
                return;
        }

        var buffer = new StringBuilder();
        WriteLeafSub(buffer, key, n);
        EnqueueProto(Encoding.ASCII.GetBytes(buffer.ToString()));
    }

    internal void WriteLeafSub(StringBuilder writer, string key, int n)
    {
        if (string.IsNullOrWhiteSpace(key))
            return;

        if (n > 0)
        {
            writer.Append("LS+ ").Append(key);
            if (key.Contains(' '))
                writer.Append(' ').Append(n);
        }
        else
        {
            writer.Append("LS- ").Append(key);
        }

        writer.Append("\r\n");
    }

    internal Exception? ProcessLeafSub(byte[] protoArg)
    {
        _in.Subs++;
        if (Server is not NatsServer server)
            return null;

        var args = SplitArg(protoArg);
        if (args.Count is not 1 and not 3)
            return new FormatException($"processLeafSub Parse Error: '{Encoding.ASCII.GetString(protoArg)}'");

        var key = Encoding.ASCII.GetString(args[0]);
        var keyParts = key.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        if (keyParts.Length == 0)
            return new FormatException($"processLeafSub Parse Error: '{key}'");

        var subject = Encoding.ASCII.GetBytes(keyParts[0]);
        byte[]? queue = null;
        if (keyParts.Length > 1)
            queue = Encoding.ASCII.GetBytes(keyParts[1]);

        var qw = 1;
        if (args.Count == 3)
        {
            if (!int.TryParse(Encoding.ASCII.GetString(args[2]), out qw) || qw <= 0)
                qw = 1;
        }

        if (_account is not Account account)
            return null;

        lock (_mu)
        {
            if (IsClosed())
                return null;

            var subjectText = Encoding.ASCII.GetString(subject);
            if (Perms is not null && !CanExport(subjectText))
            {
                LeafSubPermViolation(subject);
                return null;
            }

            if (SubsAtLimit())
            {
                MaxSubsExceeded();
                return null;
            }

            if (Subs.ContainsKey(key))
                return null;

            var sub = new Internal.Subscription
            {
                Subject = subject,
                Queue = queue,
                Sid = Encoding.ASCII.GetBytes(key),
                Qw = qw,
            };
            Subs[key] = sub;
            account.Sublist?.Insert(sub);
        }

        var subValue = Subs[key];
        var delta = queue is { Length: > 0 } ? qw : 1;
        if (!IsSpokeLeafNode())
        {
            server.UpdateRemoteSubscription(account, subValue, delta);
            if (server.GetOpts().Gateway.Name.Length > 0)
                server.UpdateInterestForAccountOnGateway(account.GetName(), subValue, delta);
        }

        account.UpdateLeafNodes(subValue, delta);

        if (Opts.Verbose)
            SendOK();
        return null;
    }

    internal Exception? HandleLeafNodeLoop(bool detectedLocally)
    {
        _ = detectedLocally;
        var (accountName, delay) = SetLeafConnectDelayIfSoliciting(LeafNodeHandler.LeafNodeReconnectDelayAfterLoopDetected);
        if (string.IsNullOrWhiteSpace(accountName))
            return null;

        Warnf("Detected loop in leafnode setup for account {0}, reconnect delayed by {1}", accountName, delay);
        return null;
    }

    internal Exception? ProcessLeafUnsub(byte[] arg)
    {
        _in.Subs++;

        if (Server is not NatsServer server)
            return null;

        Internal.Subscription? sub;
        var key = Encoding.ASCII.GetString(arg);
        var spoke = false;

        lock (_mu)
        {
            if (IsClosed())
                return null;

            spoke = IsSpokeLeafNode();
            if (!Subs.TryGetValue(key, out sub))
                return null;

            Subs.Remove(key);
        }

        var account = _account as Account;
        if (account is null || sub is null)
            return null;

        account.Sublist?.Remove(sub);

        var delta = sub.Queue is { Length: > 0 } ? Math.Max(sub.Qw, 1) : 1;
        if (!spoke)
        {
            server.UpdateRemoteSubscription(account, sub, -delta);
            if (server.GetOpts().Gateway.Name.Length > 0)
                server.UpdateInterestForAccountOnGateway(account.GetName(), sub, -delta);
        }

        account.UpdateLeafNodes(sub, -delta);
        return null;
    }

    internal Exception? ProcessLeafHeaderMsgArgs(byte[] arg) =>
        ProtocolParser.ProcessLeafHeaderMsgArgs(ParseCtx, arg);

    internal Exception? ProcessLeafMsgArgs(byte[] arg) =>
        ProtocolParser.ProcessLeafMsgArgs(ParseCtx, arg);

    internal void ProcessInboundLeafMsg(byte[] msg)
    {
        ProcessInboundRoutedMsg(msg);
    }

    internal void LeafSubPermViolation(byte[] subject) => LeafPermViolation(pub: false, subject);

    internal void LeafPermViolation(bool pub, byte[] subject)
    {
        if (IsSpokeLeafNode())
            return;

        SetLeafConnectDelayIfSoliciting(LeafNodeHandler.LeafNodeReconnectAfterPermViolation);
        var subjectText = Encoding.ASCII.GetString(subject);
        if (pub)
        {
            SendErr($"Permissions Violation for Publish to '{subjectText}'");
            Errorf("Publish Violation on '{0}' - Check other side configuration", subjectText);
        }
        else
        {
            SendErr($"Permissions Violation for Subscription to '{subjectText}'");
            Errorf("Subscription Violation on '{0}' - Check other side configuration", subjectText);
        }

        CloseConnection(ClosedState.ProtocolViolation);
    }

    internal void LeafProcessErr(string errorText)
    {
        if (errorText.Contains(ServerErrors.ErrLeafNodeHasSameClusterName.Message, StringComparison.Ordinal))
        {
            var (_, delay) = SetLeafConnectDelayIfSoliciting(LeafNodeHandler.LeafNodeReconnectDelayAfterClusterNameSame);
            Errorf("Leafnode connection dropped with same cluster name error. Delaying reconnect for {0}", delay);
            return;
        }

        if (errorText.Contains("Loop detected", StringComparison.OrdinalIgnoreCase))
            _ = HandleLeafNodeLoop(detectedLocally: false);
    }

    internal (string AccountName, TimeSpan Delay) SetLeafConnectDelayIfSoliciting(TimeSpan delay)
    {
        lock (_mu)
        {
            if (IsSolicitedLeafNode())
                Leaf?.Remote?.SetConnectDelay(delay);

            return (_account?.Name ?? string.Empty, delay);
        }
    }

    internal (bool TlsRequired, SslServerAuthenticationOptions? TlsConfig, string TlsName, double TlsTimeout) LeafNodeGetTLSConfigForSolicit(LeafNodeCfg remote)
    {
        remote.AcquireReadLock();
        try
        {
            var opts = remote.RemoteOpts;
            var tlsRequired = opts?.Tls == true || opts?.TlsConfig is not null;
            return (tlsRequired, opts?.TlsConfig, remote.TlsName, opts?.TlsTimeout ?? ServerConstants.TlsTimeout.TotalSeconds);
        }
        finally
        {
            remote.ReleaseReadLock();
        }
    }

    internal (byte[]? PreBuffer, ClosedState CloseReason, Exception? Error) LeafNodeSolicitWSConnection(ServerOptions options, Uri remoteUrl, LeafNodeCfg remote)
    {
        _ = options;
        _ = remote;

        if (!string.Equals(remoteUrl.Scheme, "ws", StringComparison.OrdinalIgnoreCase)
            && !string.Equals(remoteUrl.Scheme, "wss", StringComparison.OrdinalIgnoreCase))
            return (null, ClosedState.ProtocolViolation, new InvalidOperationException("URL is not websocket based"));

        return (null, ClosedState.ClientClosed, null);
    }
}
