// Copyright 2018-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0

using System.Security.Cryptography;
using System.Text;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server;

internal static class GatewayHandler
{
    internal static readonly byte[] GwReplyPrefix = Encoding.ASCII.GetBytes("_GR_.");
    internal static readonly byte[] OldGwReplyPrefix = Encoding.ASCII.GetBytes("$GR.");
    internal const int GwHashLen = 6;
    internal const int GwClusterOffset = 5;
    internal const int GwServerOffset = GwClusterOffset + GwHashLen + 1;
    internal const int GwSubjectOffset = GwServerOffset + GwHashLen + 1;
    internal const int OldGwReplyPrefixLen = 4;
    internal const int OldGwReplyStart = OldGwReplyPrefixLen + 5;
    internal const int GatewayCmdAllSubsStart = 2;
    internal const int GatewayCmdAllSubsComplete = 3;
    internal const int GatewayMaxRUnsubBeforeSwitch = 1000;

    private static readonly TimeSpan DefaultSolicitGatewaysDelay = TimeSpan.FromSeconds(1);
    private static long _gatewaySolicitDelayTicks = DefaultSolicitGatewaysDelay.Ticks;
    private static int _doNotForceInterestOnlyMode;

    internal static void SetGatewaysSolicitDelay(TimeSpan delay)
    {
        Interlocked.Exchange(ref _gatewaySolicitDelayTicks, delay.Ticks);
    }

    internal static void ResetGatewaysSolicitDelay()
    {
        Interlocked.Exchange(ref _gatewaySolicitDelayTicks, DefaultSolicitGatewaysDelay.Ticks);
    }

    internal static TimeSpan GetGatewaysSolicitDelay()
    {
        return TimeSpan.FromTicks(Interlocked.Read(ref _gatewaySolicitDelayTicks));
    }

    internal static void GatewayDoNotForceInterestOnlyMode(bool doNotForce)
    {
        Interlocked.Exchange(ref _doNotForceInterestOnlyMode, doNotForce ? 1 : 0);
    }

    internal static bool DoNotForceInterestOnlyMode()
    {
        return Interlocked.CompareExchange(ref _doNotForceInterestOnlyMode, 0, 0) != 0;
    }

    internal static Exception? ValidateGatewayOptions(ServerOptions options)
    {
        var gateway = options.Gateway;
        if (string.IsNullOrWhiteSpace(gateway.Name) || gateway.Port == 0)
            return null;

        if (gateway.Name.Contains(' '))
            return ServerErrors.ErrGatewayNameHasSpaces;

        var names = new HashSet<string>(StringComparer.Ordinal);
        foreach (var remote in gateway.Gateways)
        {
            if (string.IsNullOrWhiteSpace(remote.Name))
                return new InvalidOperationException("gateway remote requires a name");

            if (!names.Add(remote.Name))
                return new InvalidOperationException($"duplicate gateway remote: {remote.Name}");

            if (remote.Urls.Count == 0)
                return new InvalidOperationException($"gateway remote {remote.Name} has no URLs");
        }

        return null;
    }

    internal static byte[] GetGWHash(string value) => GetHash(value, 6);

    internal static byte[] GetOldHash(string value) => GetHash(value, 4);

    internal static byte[] GwBuildSubProto(string accountName, Subscription subscription, bool isUnsub = false)
    {
        var op = isUnsub ? "RS-" : "RS+";
        var subject = Encoding.ASCII.GetString(subscription.Subject);
        if (subscription.Queue is { Length: > 0 } queue)
            return Encoding.ASCII.GetBytes($"{op} {accountName} {subject} {Encoding.ASCII.GetString(queue)}\r\n");

        return Encoding.ASCII.GetBytes($"{op} {accountName} {subject}\r\n");
    }

    internal static bool IsGWRoutedReply(byte[] subject)
    {
        return subject.AsSpan().StartsWith(GwReplyPrefix) || subject.AsSpan().StartsWith(OldGwReplyPrefix);
    }

    internal static (bool IsRouted, bool IsOldPrefix) IsGWRoutedSubjectAndIsOldPrefix(byte[] subject)
    {
        if (subject.AsSpan().StartsWith(OldGwReplyPrefix))
            return (true, true);
        if (subject.AsSpan().StartsWith(GwReplyPrefix))
            return (true, false);
        return (false, false);
    }

    internal static bool HasGWRoutedReplyPrefix(byte[] subject)
    {
        return IsGWRoutedReply(subject);
    }

    internal static byte[] GetSubjectFromGWRoutedReply(byte[] reply, bool isOldPrefix)
    {
        if (isOldPrefix)
            return reply.Length > OldGwReplyStart ? reply[OldGwReplyStart..] : [];

        return reply.Length > GwSubjectOffset ? reply[GwSubjectOffset..] : [];
    }

    internal static string GetAccountFromGatewayCommand(ClientConnection connection, ServerInfo info, string command)
    {
        if (info.GatewayCmdPayload == null || info.GatewayCmdPayload.Length == 0)
        {
            connection.SendErrAndErr($"Account absent from receive-all-subscriptions-{command} command");
            connection.CloseConnection(ClosedState.ProtocolViolation);
            return string.Empty;
        }

        return Encoding.ASCII.GetString(info.GatewayCmdPayload);
    }

    private static byte[] GetHash(string value, int len)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        var hash = SHA256.HashData(bytes);
        var encoded = Convert.ToBase64String(hash)
            .Replace('+', '-')
            .Replace('/', '_')
            .TrimEnd('=');

        return Encoding.ASCII.GetBytes(encoded[..len]);
    }
}
