using MemberCenter.Api.Extensions;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using MemberCenter.Infrastructure.Identity;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
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 const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IOpenIddictApplicationManager _applicationManager;
    private readonly IAuthResourceRegistryService _authResourceRegistry;
    private readonly ISecuritySettingsService _securitySettingsService;

    public TokenController(
        UserManager<ApplicationUser> userManager,
        IOpenIddictApplicationManager applicationManager,
        IAuthResourceRegistryService authResourceRegistry,
        ISecuritySettingsService securitySettingsService)
    {
        _userManager = userManager;
        _applicationManager = applicationManager;
        _authResourceRegistry = authResourceRegistry;
        _securitySettingsService = securitySettingsService;
    }

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

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

            var principal = authenticateResult.Principal;
            if (!await ValidateUserPrincipalAsync(principal))
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            await ApplyTokenLifetimesAsync(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);
                }
            }

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

            if (string.IsNullOrWhiteSpace(usage))
            {
                return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            }

            var principal = new ClaimsPrincipal(identity);
            var scopes = await _authResourceRegistry.GetAllowedScopesForUsageAsync(usage, HttpContext.RequestAborted);
            principal.SetScopes(scopes);
            principal.SetResources(await ResolveResourcesAsync(scopes));

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

            await ApplyTokenLifetimesAsync(principal);
            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

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

    private async Task<bool> ValidateUserPrincipalAsync(ClaimsPrincipal principal)
    {
        var subject = principal.GetClaim(OpenIddictConstants.Claims.Subject)
            ?? principal.FindFirstValue(ClaimTypes.NameIdentifier);
        if (!Guid.TryParse(subject, out var userId))
        {
            return true;
        }

        var user = await _userManager.FindByIdAsync(userId.ToString());
        if (user is null || user.DisabledAt.HasValue)
        {
            return false;
        }

        var tokenSecurityStamp = principal.FindFirst(SecurityStampClaimType)?.Value;
        return string.IsNullOrWhiteSpace(tokenSecurityStamp)
            || string.Equals(tokenSecurityStamp, user.SecurityStamp, StringComparison.Ordinal);
    }

    private async Task<IReadOnlyList<string>> ResolveResourcesAsync(IEnumerable<string> scopes)
    {
        return await _authResourceRegistry.ResolveAudiencesAsync(scopes, HttpContext.RequestAborted);
    }

    private async Task ApplyTokenLifetimesAsync(ClaimsPrincipal principal)
    {
        var settings = await _securitySettingsService.GetTokenLifetimeSettingsAsync();
        var accessMinutes = Math.Clamp(settings.AccessTokenMinutes, 5, 1440);
        var refreshDays = Math.Clamp(settings.RefreshTokenDays, 1, 365);
        principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(accessMinutes));
        principal.SetRefreshTokenLifetime(TimeSpan.FromDays(refreshDays));
    }
}
