using System.IdentityModel.Tokens.Jwt;
using System.Text;
using e_suite.API.Common.exceptions;
using e_suite.API.Common.extensions;
using e_suite.Database.Core.Tables.UserManager;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;

namespace eSuite.API.SingleSignOn;

/// <summary>
/// Internal class used to manage the single sign on process
/// </summary>
public class SingleSignOn : ISingleSignOn
{
    private readonly e_suite.API.Common.IUserManager _userManager;
    private readonly IConfiguration _configuration;
    private readonly IHttpClientFacade _httpClientFacade;

    /// <summary>
    /// Default constructor used for DI
    /// </summary>
    /// <param name="userManager"></param>
    /// <param name="configuration"></param>
    /// <param name="httpClientFacade"></param>
    public SingleSignOn(e_suite.API.Common.IUserManager userManager, IConfiguration configuration, IHttpClientFacade httpClientFacade)
    {
        _userManager = userManager;
        _configuration = configuration;
        _httpClientFacade = httpClientFacade;
    }

    private readonly string _responseType = "code";
    private readonly string _scope = "openid";

    private string GetRedirectUri(SsoProvider ssoProvider)
    {
        var redirectUri = _configuration.GetConfigValue("BASE_URL", "baseUrl", "http://localhost:3000");

        return $"{redirectUri!.TrimEnd('/')}/account/auth/{ssoProvider.Id}";
    }

    /// <summary>
    /// Use an e-mail address to find the appropriate sso provider and start the single sign on process.
    /// </summary>
    /// <param name="loginEmail"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<string> StartSingleSignOn(string loginEmail, CancellationToken cancellationToken)
    {
        SsoProvider? ssoProvider;
        try
        {
            ssoProvider = await GetSsoProviderByEmail(loginEmail, cancellationToken);
        }
        catch (NotFoundException)
        {
            ssoProvider = null;
        }

        return BuildAuthorizationUrl(ssoProvider);
    }

    /// <summary>
    /// Use a known sso provider id to start the single sign on process.
    /// </summary>
    /// <param name="ssoProviderId"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    /// <exception cref="NotFoundException"></exception>
    public async Task<string> StartSingleSignOn(long ssoProviderId, CancellationToken cancellationToken)
    {
        var ssoProvider = await _userManager.GetSsoProviderById(ssoProviderId, cancellationToken) ??
                          throw new NotFoundException("SSO Provider not found");

        return BuildAuthorizationUrl(ssoProvider);
    }

    private string BuildAuthorizationUrl(SsoProvider? ssoProvider)
    {
        if (ssoProvider == null)
            return string.Empty;

        var authorizationUrl =
            $"{ssoProvider.AuthorizationEndpoint}?response_type={_responseType}&client_id={ssoProvider.ClientId}&redirect_uri={GetRedirectUri(ssoProvider)}&scope={_scope}";
        return authorizationUrl;
    }

    private async Task<SsoProvider?> GetSsoProviderByEmail(string loginEmail, CancellationToken cancellationToken)
    {
        var ssoProvider = await _userManager.GetSsoProviderForEmail(loginEmail, cancellationToken) ?? throw new NotFoundException("SSO Provider Not Found");
        return ssoProvider;
    }

    /// <summary>
    /// Exchange the Authorisation Token for a Certified JWT and extract the Subject
    /// </summary>
    /// <param name="ssoProviderId"></param>
    /// <param name="code"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    /// <exception cref="NotFoundException"></exception>
    public async Task<string> ExchangeAuthorisationToken(long ssoProviderId, string code, CancellationToken cancellationToken)
    {
        var ssoProvider = await _userManager.GetSsoProviderById(ssoProviderId, cancellationToken) ?? throw new NotFoundException("SSO Provider Not Found");

        var formFields = new Dictionary<string, string>
        {
            { "grant_type", "authorization_code" },
            { "code", code },
            { "redirect_uri", GetRedirectUri(ssoProvider) },
            { "client_id", ssoProvider.ClientId },
            { "client_secret", ssoProvider.ClientSecret }
        };

        using var request = new HttpRequestMessage(HttpMethod.Post, ssoProvider.TokenEndpoint);
        request.Content = new FormUrlEncodedContent(formFields);
        using var response = await _httpClientFacade.SendAsync(request, cancellationToken);
        var responseString = await response.Content.ReadAsStringAsync(cancellationToken);
        var token = ExtractAccessTokenFromResponse(responseString);

        var ssoUserId = await ExtractSubjectFromJwt(ssoProvider, token, cancellationToken);

        return ssoUserId;
    }

    private async Task<string> ExtractSubjectFromJwt(SsoProvider ssoProvider, string token, CancellationToken cancellationToken)
    {
        var validatedToken = await ValidateToken(ssoProvider, token, cancellationToken);

        return validatedToken.Subject;
    }

    /// <summary>
    /// 
    /// </summary>
    public string? ManualKey { get; set; } = null;
    
    private async Task<JwtSecurityToken> ValidateToken(SsoProvider ssoProvider, string token, CancellationToken cancellationToken)
    {
        var securityToken = new JwtSecurityToken(token);
        SecurityKey? key = await GetSecurityKey(ssoProvider, securityToken, cancellationToken);

        if (key == null)
        {
            if (ManualKey != null)
                key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ManualKey));
        }

        var tvp = new TokenValidationParameters
        {
            ValidateActor = false, // check the profile ID

            ValidateAudience = !string.IsNullOrWhiteSpace(ssoProvider.ClientId),
            ValidAudience = ssoProvider.ClientId,
            ValidateIssuer = true,
            ValidIssuer = ssoProvider.ValidIssuer,
            ValidateIssuerSigningKey = key != null,
            IssuerSigningKey = key,
            RequireSignedTokens = key != null,
            ValidateLifetime = true,
            RequireExpirationTime = true
        };
        var handler = new JwtSecurityTokenHandler();
        handler.ValidateToken(token, tvp, out var validatedToken);

        if (validatedToken is JwtSecurityToken jwtSecurityToken)
        {
            return jwtSecurityToken;
        }

        throw new ArgumentOutOfRangeException( nameof(token), "Invalid security token.");

    }

    private async Task<JsonWebKey> GetSecurityKey(SsoProvider ssoProvider, JwtSecurityToken securityToken, CancellationToken cancellationToken)
    {
        var certificates = await GetCertificateDictionary(ssoProvider, cancellationToken);
        if (certificates.Count == 0)
            return null!;
        var cert = certificates[securityToken.Header.Kid];
        return cert;
    }

    private async Task<Dictionary<string, JsonWebKey>> GetCertificateDictionary(SsoProvider ssoProvider, CancellationToken cancellationToken)
    {
        var openConfigurationUri =
            new Uri($"{ssoProvider.ValidIssuer.TrimEnd('/')}/.well-known/openid-configuration");
        var configurationJson = await _httpClientFacade.GetStringAsync(openConfigurationUri, cancellationToken);
        var openIdConfiguration = JsonConvert.DeserializeObject<OpenIdConfiguration>(configurationJson) ??
                                  throw new NullReferenceException();

        var jwks = await _httpClientFacade.GetStringAsync(new Uri(openIdConfiguration.JwksUri), cancellationToken);

        var openIdKeys = JsonConvert.DeserializeObject<OpenIdKeys>(jwks)!;

        return openIdKeys.Keys.ToDictionary(key => key.Kid);
    }

    private static string ExtractAccessTokenFromResponse(string responseString)
    {
        var responseJson = JsonConvert.DeserializeObject<OpenIdResponse>(responseString) ?? throw new NullReferenceException();

        return responseJson.IdToken;
    }
}