using System.Collections.Generic;
using System.Linq;
using OpenIddict.Abstractions;

namespace StellaOps.Authority.OpenIddict;

internal static class TokenRequestTamperInspector
{
    private static readonly HashSet<string> CommonParameters = new(StringComparer.OrdinalIgnoreCase)
    {
        OpenIddictConstants.Parameters.GrantType,
        OpenIddictConstants.Parameters.Scope,
        OpenIddictConstants.Parameters.Resource,
        OpenIddictConstants.Parameters.ClientId,
        OpenIddictConstants.Parameters.ClientSecret,
        OpenIddictConstants.Parameters.ClientAssertion,
        OpenIddictConstants.Parameters.ClientAssertionType,
        OpenIddictConstants.Parameters.RefreshToken,
        OpenIddictConstants.Parameters.DeviceCode,
        OpenIddictConstants.Parameters.Code,
        OpenIddictConstants.Parameters.CodeVerifier,
        OpenIddictConstants.Parameters.CodeChallenge,
        OpenIddictConstants.Parameters.CodeChallengeMethod,
        OpenIddictConstants.Parameters.RedirectUri,
        OpenIddictConstants.Parameters.Assertion,
        OpenIddictConstants.Parameters.Nonce,
        OpenIddictConstants.Parameters.Prompt,
        OpenIddictConstants.Parameters.MaxAge,
        OpenIddictConstants.Parameters.UiLocales,
        OpenIddictConstants.Parameters.AcrValues,
        OpenIddictConstants.Parameters.LoginHint,
        OpenIddictConstants.Parameters.Claims,
        OpenIddictConstants.Parameters.Token,
        OpenIddictConstants.Parameters.TokenTypeHint,
        OpenIddictConstants.Parameters.AccessToken,
        OpenIddictConstants.Parameters.IdToken
    };

    private static readonly HashSet<string> PasswordGrantParameters = new(StringComparer.OrdinalIgnoreCase)
    {
        OpenIddictConstants.Parameters.Username,
        OpenIddictConstants.Parameters.Password,
        AuthorityOpenIddictConstants.ProviderParameterName
    };

    private static readonly HashSet<string> ClientCredentialsParameters = new(StringComparer.OrdinalIgnoreCase)
    {
        AuthorityOpenIddictConstants.ProviderParameterName
    };

    internal static IReadOnlyList<string> GetUnexpectedPasswordGrantParameters(OpenIddictRequest request)
        => DetectUnexpectedParameters(request, PasswordGrantParameters);

    internal static IReadOnlyList<string> GetUnexpectedClientCredentialsParameters(OpenIddictRequest request)
        => DetectUnexpectedParameters(request, ClientCredentialsParameters);

    private static IReadOnlyList<string> DetectUnexpectedParameters(
        OpenIddictRequest request,
        HashSet<string> grantSpecific)
    {
        if (request is null)
        {
            return Array.Empty<string>();
        }

        var unexpected = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var pair in request.GetParameters())
        {
            var name = pair.Key;
            if (string.IsNullOrWhiteSpace(name))
            {
                continue;
            }

            if (IsAllowed(name, grantSpecific))
            {
                continue;
            }

            unexpected.Add(name);
        }

        return unexpected.Count == 0
            ? Array.Empty<string>()
            : unexpected
                .OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
                .ToArray();
    }

    private static bool IsAllowed(string parameterName, HashSet<string> grantSpecific)
    {
        if (CommonParameters.Contains(parameterName) || grantSpecific.Contains(parameterName))
        {
            return true;
        }

        if (parameterName.StartsWith("ext_", StringComparison.OrdinalIgnoreCase) ||
            parameterName.StartsWith("x-", StringComparison.OrdinalIgnoreCase) ||
            parameterName.StartsWith("custom_", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        if (parameterName.Contains(':', StringComparison.Ordinal))
        {
            return true;
        }

        return false;
    }
}
