#nullable enable

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Web;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Storage.Queues.Models;
using Bit.Core.AdminConsole.Context;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums;
using Bit.Core.Billing.Enums;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Identity;
using Bit.Core.Settings;
using IdentityModel;
using Microsoft.AspNetCore.DataProtection;
using MimeKit;

namespace Bit.Core.Utilities;

public static class CoreHelpers
{
    private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;
    private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    private static readonly Random _random = new Random();
    private static readonly string RealConnectingIp = "X-Connecting-IP";
    private static readonly Regex _whiteSpaceRegex = new Regex(@"\s+");
    private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };

    /// <summary>
    /// Generate sequential Guid for Sql Server.
    /// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs
    /// </summary>
    /// <returns>A comb Guid.</returns>
    public static Guid GenerateComb()
        => GenerateComb(Guid.NewGuid(), DateTime.UtcNow);

    /// <summary>
    /// Implementation of <see cref="GenerateComb()" /> with input parameters to remove randomness.
    /// This should NOT be used outside of testing.
    /// </summary>
    /// <remarks>
    /// You probably don't want to use this method and instead want to use <see cref="GenerateComb()" /> with no parameters
    /// </remarks>
    internal static Guid GenerateComb(Guid startingGuid, DateTime time)
    {
        var guidArray = startingGuid.ToByteArray();

        // Get the days and milliseconds which will be used to build the byte string
        var days = new TimeSpan(time.Ticks - _baseDateTicks);
        var msecs = time.TimeOfDay;

        // Convert to a byte array
        // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333
        var daysArray = BitConverter.GetBytes(days.Days);
        var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));

        // Reverse the bytes to match SQL Servers ordering
        Array.Reverse(daysArray);
        Array.Reverse(msecsArray);

        // Copy the bytes into the guid
        Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);
        Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);

        return new Guid(guidArray);
    }

    internal static DateTime DateFromComb(Guid combGuid)
    {
        var guidArray = combGuid.ToByteArray();
        var daysArray = new byte[4];
        var msecsArray = new byte[4];

        Array.Copy(guidArray, guidArray.Length - 6, daysArray, 2, 2);
        Array.Copy(guidArray, guidArray.Length - 4, msecsArray, 0, 4);

        Array.Reverse(daysArray);
        Array.Reverse(msecsArray);

        var days = BitConverter.ToInt32(daysArray, 0);
        var msecs = BitConverter.ToInt32(msecsArray, 0);

        var time = TimeSpan.FromDays(days) + TimeSpan.FromMilliseconds(msecs * 3.333333);
        return new DateTime(_baseDateTicks + time.Ticks, DateTimeKind.Utc);
    }

    internal static long BinForComb(Guid combGuid, int binCount)
    {
        // From System.Web.Util.HashCodeCombiner
        uint CombineHashCodes(uint h1, byte h2)
        {
            return (uint)(((h1 << 5) + h1) ^ h2);
        }
        var guidArray = combGuid.ToByteArray();
        var randomArray = new byte[10];
        Array.Copy(guidArray, 0, randomArray, 0, 10);
        var hash = randomArray.Aggregate((uint)randomArray.Length, CombineHashCodes);
        return hash % binCount;
    }

    public static string CleanCertificateThumbprint(string thumbprint)
    {
        // Clean possible garbage characters from thumbprint copy/paste
        // ref http://stackoverflow.com/questions/8448147/problems-with-x509store-certificates-find-findbythumbprint
        return Regex.Replace(thumbprint, @"[^\da-fA-F]", string.Empty).ToUpper();
    }

    public static X509Certificate2? GetCertificate(string thumbprint)
    {
        thumbprint = CleanCertificateThumbprint(thumbprint);

        X509Certificate2? cert = null;
        var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        certStore.Open(OpenFlags.ReadOnly);
        var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
        if (certCollection.Count > 0)
        {
            cert = certCollection[0];
        }

        certStore.Close();
        return cert;
    }

    public static X509Certificate2 GetCertificate(string file, string password)
    {
        return new X509Certificate2(file, password);
    }

    public async static Task<X509Certificate2> GetEmbeddedCertificateAsync(string file, string password)
    {
        var assembly = typeof(CoreHelpers).GetTypeInfo().Assembly;
        using (var s = assembly.GetManifestResourceStream($"Bit.Core.{file}")!)
        using (var ms = new MemoryStream())
        {
            await s.CopyToAsync(ms);
            return new X509Certificate2(ms.ToArray(), password);
        }
    }

    public static string GetEmbeddedResourceContentsAsync(string file)
    {
        var assembly = Assembly.GetCallingAssembly();
        var resourceName = assembly.GetManifestResourceNames().Single(n => n.EndsWith(file));
        using (var stream = assembly.GetManifestResourceStream(resourceName)!)
        using (var reader = new StreamReader(stream))
        {
            return reader.ReadToEnd();
        }
    }

    public async static Task<X509Certificate2?> GetBlobCertificateAsync(string connectionString, string container, string file, string password)
    {
        try
        {
            var blobServiceClient = new BlobServiceClient(connectionString);
            var containerRef2 = blobServiceClient.GetBlobContainerClient(container);
            var blobRef = containerRef2.GetBlobClient(file);

            using var memStream = new MemoryStream();
            await blobRef.DownloadToAsync(memStream).ConfigureAwait(false);
            return new X509Certificate2(memStream.ToArray(), password);
        }
        catch (RequestFailedException ex)
        when (ex.ErrorCode == BlobErrorCode.ContainerNotFound || ex.ErrorCode == BlobErrorCode.BlobNotFound)
        {
            return null;
        }
        catch (Exception)
        {
            return null;
        }
    }

    public static long ToEpocMilliseconds(DateTime date)
    {
        return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
    }

    public static DateTime FromEpocMilliseconds(long milliseconds)
    {
        return _epoc.AddMilliseconds(milliseconds);
    }

    public static long ToEpocSeconds(DateTime date)
    {
        return (long)Math.Round((date - _epoc).TotalSeconds, 0);
    }

    public static DateTime FromEpocSeconds(long seconds)
    {
        return _epoc.AddSeconds(seconds);
    }

    public static string U2fAppIdUrl(GlobalSettings globalSettings)
    {
        return string.Concat(globalSettings.BaseServiceUri.Vault, "/app-id.json");
    }

    public static string RandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
        bool numeric = true, bool special = false)
    {
        return RandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
    }

    public static string RandomString(int length, string characters)
    {
        return new string(Enumerable.Repeat(characters, length).Select(s => s[_random.Next(s.Length)]).ToArray());
    }

    public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
        bool numeric = true, bool special = false)
    {
        return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
    }

    // ref https://stackoverflow.com/a/8996788/1090359 with modifications
    public static string SecureRandomString(int length, string characters)
    {
        if (length < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(length), "length cannot be less than zero.");
        }

        if (string.IsNullOrEmpty(characters))
        {
            throw new ArgumentOutOfRangeException(nameof(characters), "characters invalid.");
        }

        const int byteSize = 0x100;
        if (byteSize < characters.Length)
        {
            throw new ArgumentException(
                string.Format("{0} may contain no more than {1} characters.", nameof(characters), byteSize),
                nameof(characters));
        }

        var outOfRangeStart = byteSize - (byteSize % characters.Length);
        using (var rng = RandomNumberGenerator.Create())
        {
            var sb = new StringBuilder();
            var buffer = new byte[128];
            while (sb.Length < length)
            {
                rng.GetBytes(buffer);
                for (var i = 0; i < buffer.Length && sb.Length < length; ++i)
                {
                    // Divide the byte into charSet-sized groups. If the random value falls into the last group and the
                    // last group is too small to choose from the entire allowedCharSet, ignore the value in order to
                    // avoid biasing the result.
                    if (outOfRangeStart <= buffer[i])
                    {
                        continue;
                    }

                    sb.Append(characters[buffer[i] % characters.Length]);
                }
            }

            return sb.ToString();
        }
    }

    private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special)
    {
        var characters = string.Empty;
        if (alpha)
        {
            if (upper)
            {
                characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            }

            if (lower)
            {
                characters += "abcdefghijklmnopqrstuvwxyz";
            }
        }

        if (numeric)
        {
            characters += "0123456789";
        }

        if (special)
        {
            characters += "!@#$%^*&";
        }

        return characters;
    }

    // ref: https://stackoverflow.com/a/11124118/1090359
    // Returns the human-readable file size for an arbitrary 64-bit file size .
    // The format is "0.## XB", ex: "4.2 KB" or "1.43 GB"
    public static string ReadableBytesSize(long size)
    {
        // Get absolute value
        var absoluteSize = (size < 0 ? -size : size);

        // Determine the suffix and readable value
        string suffix;
        double readable;
        if (absoluteSize >= 0x40000000) // 1 Gigabyte
        {
            suffix = "GB";
            readable = (size >> 20);
        }
        else if (absoluteSize >= 0x100000) // 1 Megabyte
        {
            suffix = "MB";
            readable = (size >> 10);
        }
        else if (absoluteSize >= 0x400) // 1 Kilobyte
        {
            suffix = "KB";
            readable = size;
        }
        else
        {
            return size.ToString("0 Bytes"); // Byte
        }

        // Divide by 1024 to get fractional value
        readable = (readable / 1024);

        // Return formatted number with suffix
        return readable.ToString("0.## ") + suffix;
    }

    /// <summary>
    /// Creates a clone of the given object through serializing to json and deserializing.
    /// This method is subject to the limitations of System.Text.Json. For example, properties with
    /// inaccessible setters will not be set.
    /// </summary>
    public static T CloneObject<T>(T obj)
    {
        return JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(obj))!;
    }

    public static bool SettingHasValue([NotNullWhen(true)] string? setting)
    {
        var normalizedSetting = setting?.ToLowerInvariant();
        return !string.IsNullOrWhiteSpace(normalizedSetting) && !normalizedSetting.Equals("secret") &&
            !normalizedSetting.Equals("replace");
    }

    public static string Base64EncodeString(string input)
    {
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(input));
    }

    public static string Base64DecodeString(string input)
    {
        return Encoding.UTF8.GetString(Convert.FromBase64String(input));
    }

    public static string Base64UrlEncodeString(string input)
    {
        return Base64UrlEncode(Encoding.UTF8.GetBytes(input));
    }

    public static string Base64UrlDecodeString(string input)
    {
        return Encoding.UTF8.GetString(Base64UrlDecode(input));
    }

    /// <summary>
    /// Encodes a Base64 URL formatted string.
    /// </summary>
    /// <param name="input">Byte data</param>
    /// <returns>Base64 URL formatted string</returns>
    public static string Base64UrlEncode(byte[] input)
    {
        // Standard base64 encoder
        var standardB64 = Convert.ToBase64String(input);
        return TransformToBase64Url(standardB64);
    }

    /// <summary>
    /// Transforms a Base64 standard formatted string to a Base64 URL formatted string.
    /// </summary>
    /// <param name="input">Base64 standard formatted string</param>
    /// <returns>Base64 URL formatted string</returns>
    public static string TransformToBase64Url(string input)
    {
        var output = input
            .Replace('+', '-')
            .Replace('/', '_')
            .Replace("=", string.Empty);
        return output;
    }

    /// <summary>
    /// Decodes a Base64 URL formatted string.
    /// </summary>
    /// <param name="input">Base64 URL formatted string</param>
    /// <returns>Data as bytes</returns>
    public static byte[] Base64UrlDecode(string input)
    {
        var standardB64 = TransformFromBase64Url(input);
        // Standard base64 decoder
        return Convert.FromBase64String(standardB64);
    }

    /// <summary>
    /// Transforms a Base64 URL formatted string to a Base64 standard formatted string.
    /// </summary>
    /// <param name="input">Base64 URL formatted string</param>
    /// <returns>Base64 standard formatted string</returns>
    public static string TransformFromBase64Url(string input)
    {
        // TODO: .NET 9 Ships Base64Url in box, investigate replacing this usage with that
        // Ref: https://github.com/dotnet/runtime/pull/102364
        var output = input;
        // 62nd char of encoding
        output = output.Replace('-', '+');
        // 63rd char of encoding
        output = output.Replace('_', '/');
        // Pad with trailing '='s
        switch (output.Length % 4)
        {
            case 0:
                // No pad chars in this case
                break;
            case 2:
                // Two pad chars
                output += "=="; break;
            case 3:
                // One pad char
                output += "="; break;
            default:
                throw new InvalidOperationException("Illegal base64url string!");
        }

        // Standard base64 string output
        return output;
    }

    [return: NotNullIfNotNull(nameof(text))]
    public static string? PunyEncode(string? text)
    {
        if (text == "")
        {
            return "";
        }

        if (text == null)
        {
            return null;
        }

        if (!text.Contains("@"))
        {
            // Assume domain name or non-email address
            var idn = new IdnMapping();
            return idn.GetAscii(text);
        }
        else
        {
            // Assume email address
            return MailboxAddress.EncodeAddrspec(text);
        }
    }

    public static string? FormatLicenseSignatureValue(object val)
    {
        if (val == null)
        {
            return string.Empty;
        }

        if (val is DateTime dateTimeVal)
        {
            return ToEpocSeconds(dateTimeVal).ToString();
        }

        if (val is bool boolVal)
        {
            return boolVal.ToString().ToLowerInvariant();
        }

        if (val is PlanType planType)
        {
            return planType switch
            {
                PlanType.Free => "Free",
                PlanType.FamiliesAnnually2019 => "FamiliesAnnually",
                PlanType.TeamsMonthly2019 => "TeamsMonthly",
                PlanType.TeamsAnnually2019 => "TeamsAnnually",
                PlanType.EnterpriseMonthly2019 => "EnterpriseMonthly",
                PlanType.EnterpriseAnnually2019 => "EnterpriseAnnually",
                PlanType.Custom => "Custom",
                _ => ((byte)planType).ToString(),
            };
        }

        return val.ToString();
    }

    public static string SanitizeForEmail(string value, bool htmlEncode = true)
    {
        var cleanedValue = value.Replace("@", "[at]");
        var regexOptions = RegexOptions.CultureInvariant |
            RegexOptions.Singleline |
            RegexOptions.IgnoreCase;
        cleanedValue = Regex.Replace(cleanedValue, @"(\.\w)",
                m => string.Concat("[dot]", m.ToString().Last()), regexOptions);
        while (Regex.IsMatch(cleanedValue, @"((^|\b)(\w*)://)", regexOptions))
        {
            cleanedValue = Regex.Replace(cleanedValue, @"((^|\b)(\w*)://)",
                string.Empty, regexOptions);
        }
        return htmlEncode ? HttpUtility.HtmlEncode(cleanedValue) : cleanedValue;
    }

    public static string DateTimeToTableStorageKey(DateTime? date = null)
    {
        if (date.HasValue)
        {
            date = date.Value.ToUniversalTime();
        }
        else
        {
            date = DateTime.UtcNow;
        }

        return _max.Subtract(date.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
    }

    // ref: https://stackoverflow.com/a/27545010/1090359
    public static Uri ExtendQuery(Uri uri, IDictionary<string, string> values)
    {
        var baseUri = uri.ToString();
        var queryString = string.Empty;
        if (baseUri.Contains("?"))
        {
            var urlSplit = baseUri.Split('?');
            baseUri = urlSplit[0];
            queryString = urlSplit.Length > 1 ? urlSplit[1] : string.Empty;
        }

        var queryCollection = HttpUtility.ParseQueryString(queryString);
        foreach (var kvp in values ?? new Dictionary<string, string>())
        {
            queryCollection[kvp.Key] = kvp.Value;
        }

        var uriKind = uri.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative;
        if (queryCollection.Count == 0)
        {
            return new Uri(baseUri, uriKind);
        }
        return new Uri(string.Format("{0}?{1}", baseUri, queryCollection), uriKind);
    }

    public static string CustomProviderName(TwoFactorProviderType type)
    {
        return string.Concat("Custom_", type.ToString());
    }

    // TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete
    public static bool UserInviteTokenIsValid(IDataProtector protector, string token, string userEmail,
        Guid orgUserId, IGlobalSettings globalSettings)
    {
        return TokenIsValid("OrganizationUserInvite", protector, token, userEmail, orgUserId,
            globalSettings.OrganizationInviteExpirationHours);
    }

    public static bool TokenIsValid(string firstTokenPart, IDataProtector protector, string token, string userEmail,
        Guid id, double expirationInHours)
    {
        var invalid = true;
        try
        {
            var unprotectedData = protector.Unprotect(token);
            var dataParts = unprotectedData.Split(' ');
            if (dataParts.Length == 4 && dataParts[0] == firstTokenPart &&
                new Guid(dataParts[1]) == id &&
                dataParts[2].Equals(userEmail, StringComparison.InvariantCultureIgnoreCase))
            {
                var creationTime = FromEpocMilliseconds(Convert.ToInt64(dataParts[3]));
                var expTime = creationTime.AddHours(expirationInHours);
                invalid = expTime < DateTime.UtcNow;
            }
        }
        catch
        {
            invalid = true;
        }

        return !invalid;
    }

    public static string GetApplicationCacheServiceBusSubscriptionName(GlobalSettings globalSettings)
    {
        var subName = globalSettings.ServiceBus.ApplicationCacheSubscriptionName;
        if (string.IsNullOrWhiteSpace(subName))
        {
            var websiteInstanceId = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID") ??
                                    globalSettings.ServiceBus.WebSiteInstanceId;
            if (string.IsNullOrWhiteSpace(websiteInstanceId))
            {
                throw new Exception("No service bus subscription name available.");
            }
            else
            {
                subName = $"{globalSettings.ProjectName.ToLower()}_{websiteInstanceId}";
                if (subName.Length > 50)
                {
                    subName = subName.Substring(0, 50);
                }
            }
        }
        return subName;
    }

    public static string? GetIpAddress(this Microsoft.AspNetCore.Http.HttpContext httpContext,
        GlobalSettings globalSettings)
    {
        if (httpContext == null)
        {
            return null;
        }

        if (!globalSettings.SelfHosted && httpContext.Request.Headers.TryGetValue(RealConnectingIp, out var realConnectingIp))
        {
            return realConnectingIp.ToString();
        }

        return httpContext.Connection?.RemoteIpAddress?.ToString();
    }

    public static bool IsCorsOriginAllowed(string origin, GlobalSettings globalSettings)
    {
        return
            // Web vault
            origin == globalSettings.BaseServiceUri.Vault ||
            // Safari extension origin
            origin == "file://" ||
            // Product website
            (!globalSettings.SelfHosted && origin == "https://bitwarden.com");
    }

    public static X509Certificate2? GetIdentityServerCertificate(GlobalSettings globalSettings)
    {
        if (globalSettings.SelfHosted &&
            SettingHasValue(globalSettings.IdentityServer.CertificatePassword)
            && File.Exists(globalSettings.IdentityServer.CertificateLocation))
        {
            return GetCertificate(globalSettings.IdentityServer.CertificateLocation,
                globalSettings.IdentityServer.CertificatePassword);
        }
        else if (SettingHasValue(globalSettings.IdentityServer.CertificateThumbprint))
        {
            return GetCertificate(
                globalSettings.IdentityServer.CertificateThumbprint);
        }
        else if (!globalSettings.SelfHosted &&
            SettingHasValue(globalSettings.Storage?.ConnectionString) &&
            SettingHasValue(globalSettings.IdentityServer.CertificatePassword))
        {
            return GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, "certificates",
                "identity.pfx", globalSettings.IdentityServer.CertificatePassword).GetAwaiter().GetResult();
        }
        return null;
    }

    public static Dictionary<string, object> AdjustIdentityServerConfig(Dictionary<string, object> configDict,
        string publicServiceUri, string internalServiceUri)
    {
        var dictReplace = new Dictionary<string, object>();
        foreach (var item in configDict)
        {
            if (item.Key == "authorization_endpoint" && item.Value is string val)
            {
                var uri = new Uri(val);
                dictReplace.Add(item.Key, string.Concat(publicServiceUri, uri.LocalPath));
            }
            else if ((item.Key == "jwks_uri" || item.Key.EndsWith("_endpoint")) && item.Value is string val2)
            {
                var uri = new Uri(val2);
                dictReplace.Add(item.Key, string.Concat(internalServiceUri, uri.LocalPath));
            }
        }
        foreach (var replace in dictReplace)
        {
            configDict[replace.Key] = replace.Value;
        }
        return configDict;
    }

    public static List<KeyValuePair<string, string>> BuildIdentityClaims(User user, ICollection<CurrentContextOrganization> orgs,
        ICollection<CurrentContextProvider> providers, bool isPremium)
    {
        var claims = new List<KeyValuePair<string, string>>()
        {
            new(Claims.Premium, isPremium ? "true" : "false"),
            new(JwtClaimTypes.Email, user.Email),
            new(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"),
            // TODO: [https://bitwarden.atlassian.net/browse/PM-22171] Remove this since it is already added from the persisted grant
            new(Claims.SecurityStamp, user.SecurityStamp),
        };

        if (!string.IsNullOrWhiteSpace(user.Name))
        {
            claims.Add(new KeyValuePair<string, string>(JwtClaimTypes.Name, user.Name));
        }

        // Orgs that this user belongs to
        if (orgs.Any())
        {
            foreach (var group in orgs.GroupBy(o => o.Type))
            {
                switch (group.Key)
                {
                    case Enums.OrganizationUserType.Owner:
                        foreach (var org in group)
                        {
                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationOwner, org.Id.ToString()));
                        }
                        break;
                    case Enums.OrganizationUserType.Admin:
                        foreach (var org in group)
                        {
                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationAdmin, org.Id.ToString()));
                        }
                        break;
                    case Enums.OrganizationUserType.User:
                        foreach (var org in group)
                        {
                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationUser, org.Id.ToString()));
                        }
                        break;
                    case Enums.OrganizationUserType.Custom:
                        foreach (var org in group)
                        {
                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationCustom, org.Id.ToString()));
                            foreach (var (permission, claimName) in org.Permissions.ClaimsMap)
                            {
                                if (!permission)
                                {
                                    continue;
                                }

                                claims.Add(new KeyValuePair<string, string>(claimName, org.Id.ToString()));
                            }
                        }
                        break;
                    default:
                        break;
                }

                // Secrets Manager
                foreach (var org in group)
                {
                    if (org.AccessSecretsManager)
                    {
                        claims.Add(new KeyValuePair<string, string>(Claims.SecretsManagerAccess, org.Id.ToString()));
                    }
                }
            }
        }

        if (providers.Any())
        {
            foreach (var group in providers.GroupBy(o => o.Type))
            {
                switch (group.Key)
                {
                    case ProviderUserType.ProviderAdmin:
                        foreach (var provider in group)
                        {
                            claims.Add(new KeyValuePair<string, string>(Claims.ProviderAdmin, provider.Id.ToString()));
                        }
                        break;
                    case ProviderUserType.ServiceUser:
                        foreach (var provider in group)
                        {
                            claims.Add(new KeyValuePair<string, string>(Claims.ProviderServiceUser, provider.Id.ToString()));
                        }
                        break;
                }
            }
        }

        return claims;
    }

    /// <summary>
    /// Deserializes JSON data into the specified type.
    /// If the JSON data is a null reference, it will still return an instantiated class.
    /// However, if the JSON data is a string "null", it will return null.
    /// </summary>
    /// <param name="jsonData">The JSON data</param>
    /// <typeparam name="T">The type to deserialize into</typeparam>
    /// <returns></returns>
    public static T LoadClassFromJsonData<T>(string? jsonData) where T : new()
    {
        if (string.IsNullOrWhiteSpace(jsonData))
        {
            return new T();
        }

#nullable disable // TODO: Remove this and fix any callee warnings.
        return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, _jsonSerializerOptions);
#nullable enable
    }

    public static string ClassToJsonData<T>(T data)
    {
        return System.Text.Json.JsonSerializer.Serialize(data, _jsonSerializerOptions);
    }

    public static ICollection<T> AddIfNotExists<T>(this ICollection<T> list, T item)
    {
        if (list.Contains(item))
        {
            return list;
        }
        list.Add(item);
        return list;
    }

    public static string? DecodeMessageText(this QueueMessage message)
    {
        var text = message?.MessageText;
        if (string.IsNullOrWhiteSpace(text))
        {
            return text;
        }
        try
        {
            return Base64DecodeString(text);
        }
        catch
        {
            return text;
        }
    }

    public static bool FixedTimeEquals(string input1, string input2)
    {
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(input1), Encoding.UTF8.GetBytes(input2));
    }

    public static string? ObfuscateEmail(string email)
    {
        if (email == null)
        {
            return email;
        }

        var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries);

        if (emailParts.Length != 2)
        {
            return email;
        }

        var username = emailParts[0];

        if (username.Length < 2)
        {
            return email;
        }

        var sb = new StringBuilder();
        sb.Append(emailParts[0][..2]);
        for (var i = 2; i < emailParts[0].Length; i++)
        {
            sb.Append('*');
        }

        return sb.Append('@')
            .Append(emailParts[1])
            .ToString();

    }

    public static string? GetEmailDomain(string email)
    {
        if (!string.IsNullOrWhiteSpace(email))
        {
            var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries);

            if (emailParts.Length == 2)
            {
                return emailParts[1].Trim();
            }
        }

        return null;
    }

    public static string ReplaceWhiteSpace(string input, string newValue)
    {
        return _whiteSpaceRegex.Replace(input, newValue);
    }

    public static string? RedactEmailAddress(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
        {
            return null;
        }

        var emailParts = email.Split('@');

        string shownPart;
        if (emailParts[0].Length > 2 && emailParts[0].Length <= 4)
        {
            shownPart = emailParts[0].Substring(0, 1);
        }
        else if (emailParts[0].Length > 4)
        {
            shownPart = emailParts[0].Substring(0, 2);
        }
        else
        {
            shownPart = string.Empty;
        }

        string redactedPart;
        if (emailParts[0].Length > 4)
        {
            redactedPart = new string('*', emailParts[0].Length - 2);
        }
        else
        {
            redactedPart = new string('*', emailParts[0].Length - shownPart.Length);
        }

        return $"{shownPart}{redactedPart}@{emailParts[1]}";
    }
}
