using MemberCenter.Api.Extensions;
using MemberCenter.Infrastructure.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System.Security.Claims;
using System.Text.Json;

namespace MemberCenter.Api.Controllers;

[ApiController]
public class TokenController : ControllerBase
{
    private readonly string _memberCenterAudience;
    private readonly string _sendEngineAudience;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IOpenIddictApplicationManager _applicationManager;

    public TokenController(
        IConfiguration configuration,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IOpenIddictApplicationManager applicationManager)
    {
        _memberCenterAudience = configuration["Auth:MemberCenterAudience"] ?? "member_center_api";
        _sendEngineAudience = configuration["Auth:SendEngineAudience"] ?? "send_engine_api";
        _userManager = userManager;
        _signInManager = signInManager;
        _applicationManager = applicationManager;
    }

    [HttpPost("/oauth/token")]
    [HttpPost("/auth/login")]
    [HttpPost("/auth/refresh")]
    public async Task<IActionResult> Exchange()
    {
        var request = HttpContext.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction?.Request;
        if (request is null)
        {
            return BadRequest("Invalid OpenIddict request.");
        }

        if (request.IsPasswordGrantType())
        {
            var user = await _userManager.FindByEmailAsync(request.Username ?? string.Empty);
            if (user is null)
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            var valid = await _userManager.CheckPasswordAsync(user, request.Password ?? string.Empty);
            if (!valid)
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            var principal = await _signInManager.CreateUserPrincipalAsync(user);
            var scopes = request.Scope.GetScopesOrDefault();
            principal.SetScopes(scopes);
            principal.SetResources(ResolveResources(scopes));

            foreach (var claim in principal.Claims)
            {
                claim.SetDestinations(ClaimsExtensions.GetDestinations(claim));
            }

            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        if (request.IsRefreshTokenGrantType())
        {
            var authenticateResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            if (!authenticateResult.Succeeded || authenticateResult.Principal is null)
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            var principal = authenticateResult.Principal;
            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        if (request.IsClientCredentialsGrantType())
        {
            if (string.IsNullOrWhiteSpace(request.ClientId))
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            var app = await _applicationManager.FindByClientIdAsync(request.ClientId);
            if (app is null)
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            var identity = new ClaimsIdentity(
                authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
                nameType: OpenIddictConstants.Claims.Name,
                roleType: OpenIddictConstants.Claims.Role);

            identity.AddClaim(OpenIddictConstants.Claims.Subject, request.ClientId);
            identity.AddClaim(OpenIddictConstants.Claims.ClientId, request.ClientId);

            var properties = await _applicationManager.GetPropertiesAsync(app);
            if (properties.TryGetValue("tenant_id", out var tenantValue))
            {
                var tenantId = tenantValue.ValueKind == JsonValueKind.String
                    ? tenantValue.GetString()
                    : tenantValue.ToString();
                if (!string.IsNullOrWhiteSpace(tenantId))
                {
                    identity.AddClaim("tenant_id", tenantId);
                }
            }

            if (properties.TryGetValue("usage", out var usageValue))
            {
                var usage = usageValue.ValueKind == JsonValueKind.String
                    ? usageValue.GetString()
                    : usageValue.ToString();
                if (!string.IsNullOrWhiteSpace(usage))
                {
                    identity.AddClaim("client_usage", usage);
                }
            }

            var principal = new ClaimsPrincipal(identity);
            var scopes = request.Scope.GetScopesOrDefault();
            principal.SetScopes(scopes);
            principal.SetResources(ResolveResources(scopes));

            foreach (var claim in principal.Claims)
            {
                claim.SetDestinations(ClaimsExtensions.GetDestinations(claim));
            }

            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        return BadRequest("Unsupported grant type.");
    }

    private IEnumerable<string> ResolveResources(IEnumerable<string> scopes)
    {
        var scopeSet = scopes as ISet<string> ?? new HashSet<string>(scopes, StringComparer.Ordinal);
        if (scopeSet.Count == 0)
        {
            return [_memberCenterAudience];
        }

        var resources = new HashSet<string>(StringComparer.Ordinal);

        if (scopeSet.Contains("newsletter:send.write") || scopeSet.Contains("newsletter:send.read"))
        {
            resources.Add(_sendEngineAudience);
        }

        if (scopeSet.Any(scope => scope.StartsWith("newsletter:", StringComparison.Ordinal) && scope is not "newsletter:send.write" && scope is not "newsletter:send.read")
            || scopeSet.Contains(OpenIddictConstants.Scopes.OpenId)
            || scopeSet.Contains(OpenIddictConstants.Scopes.Email)
            || scopeSet.Contains(OpenIddictConstants.Scopes.Profile))
        {
            resources.Add(_memberCenterAudience);
        }

        if (resources.Count == 0)
        {
            resources.Add(_memberCenterAudience);
        }

        return resources;
    }
}
