using System.Collections.Immutable;
using System.Security.Claims;
using foodmarket.Api.Infrastructure.Tenancy;
using foodmarket.Infrastructure.Identity;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace foodmarket.Api.Controllers;

[ApiController]
public class AuthorizationController : ControllerBase
{
    private readonly SignInManager<User> _signInManager;
    private readonly UserManager<User> _userManager;
    private readonly AppDbContext _db;

    public AuthorizationController(SignInManager<User> signInManager, UserManager<User> userManager, AppDbContext db)
    {
        _signInManager = signInManager;
        _userManager = userManager;
        _db = db;
    }

    [HttpPost("~/connect/token"), Produces("application/json")]
    public async Task<IActionResult> Exchange()
    {
        var request = HttpContext.GetOpenIddictServerRequest()
            ?? throw new InvalidOperationException("OpenIddict server request is missing.");

        if (request.IsPasswordGrantType())
        {
            var user = await _userManager.FindByNameAsync(request.Username!);
            if (user is null || !user.IsActive)
            {
                return BadRequestError(Errors.InvalidGrant, "Неверный логин или пароль.");
            }

            var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password!, true);
            if (!result.Succeeded)
            {
                return BadRequestError(Errors.InvalidGrant, "Неверный логин или пароль.");
            }

            var rejection = await CheckUserStillBelongsToLiveOrgAsync(user);
            if (rejection is not null) return rejection;

            user.LastLoginAt = DateTime.UtcNow;
            await _userManager.UpdateAsync(user);

            var principal = await CreatePrincipal(user, request.GetScopes());
            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        if (request.IsRefreshTokenGrantType())
        {
            var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
            var user = info.Principal is null ? null : await _userManager.GetUserAsync(info.Principal);
            if (user is null || !user.IsActive)
            {
                return BadRequestError(Errors.InvalidGrant, "Аккаунт недоступен.");
            }

            var rejection = await CheckUserStillBelongsToLiveOrgAsync(user);
            if (rejection is not null) return rejection;

            var principal = await CreatePrincipal(user, request.GetScopes());
            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        return BadRequestError(Errors.UnsupportedGrantType, "Этот grant type не поддерживается.");
    }

    /// <summary>SuperAdmin-у org не нужна — он работает на уровне платформы.
    /// Любой другой пользователь обязан указывать на живую (не архивную)
    /// Organization. Если связь оборвана (org удалена / архивирована) —
    /// reject входа с понятным сообщением, чтобы юзер не попадал в админку
    /// с orphan-claim'ом.</summary>
    private async Task<IActionResult?> CheckUserStillBelongsToLiveOrgAsync(User user)
    {
        var roles = await _userManager.GetRolesAsync(user);
        if (roles.Contains(HttpContextTenantContext.SuperAdminRole)) return null;

        if (user.OrganizationId is null)
        {
            return BadRequestError(Errors.InvalidGrant,
                "У аккаунта нет привязки к организации. Обратитесь к администратору.");
        }
        var orgAlive = await _db.Organizations.IgnoreQueryFilters()
            .AnyAsync(o => o.Id == user.OrganizationId.Value && !o.IsArchived);
        if (!orgAlive)
        {
            return BadRequestError(Errors.InvalidGrant,
                "Организация удалена или архивирована. Обратитесь к владельцу.");
        }
        return null;
    }

    private async Task<ClaimsPrincipal> CreatePrincipal(User user, ImmutableArray<string> scopes)
    {
        var identity = new ClaimsIdentity(
            authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            nameType: Claims.Name,
            roleType: Claims.Role);

        identity.SetClaim(Claims.Subject, user.Id.ToString());
        identity.SetClaim(Claims.Name, user.UserName);
        identity.SetClaim(Claims.Email, user.Email);
        identity.SetClaim(Claims.PreferredUsername, user.UserName);

        if (user.OrganizationId.HasValue)
        {
            identity.SetClaim(HttpContextTenantContext.OrganizationClaim, user.OrganizationId.Value.ToString());
        }

        var roles = await _userManager.GetRolesAsync(user);
        identity.SetClaims(Claims.Role, roles.ToImmutableArray());

        var principal = new ClaimsPrincipal(identity);
        principal.SetScopes(scopes);
        principal.SetDestinations(claim => claim.Type switch
        {
            Claims.Name or Claims.Email or Claims.PreferredUsername or Claims.Role
                or HttpContextTenantContext.OrganizationClaim
                => [Destinations.AccessToken, Destinations.IdentityToken],
            _ => [Destinations.AccessToken],
        });
        return principal;
    }

    private BadRequestObjectResult BadRequestError(string error, string description) =>
        BadRequest(new Dictionary<string, string>
        {
            [Parameters.Error] = error,
            [Parameters.ErrorDescription] = description
        });
}

internal static class TokenValidationParameters
{
    public const string DefaultAuthenticationType = "OpenIddictServer";
}
