using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Route("api/auth/sso")]
public sealed class SsoController(
    UserManager<ApplicationUser> userManager,
    IAuthSessionService authSessionService,
    IDistributedCache cache,
    IOptions<SsoOptions> options,
    ILogger<SsoController> logger) : ControllerBase
{
    private const string BindingIntentProperty = "sso-binding-intent";
    private readonly SsoOptions _options = options.Value;

    [AllowAnonymous]
    [HttpGet("settings")]
    public ActionResult<SsoSettingsResponse> Settings() =>
        new SsoSettingsResponse(
            _options.Enabled,
            _options.DisplayName,
            EffectiveCallbackUrl());

    [AllowAnonymous]
    [EnableRateLimiting("public-auth")]
    [HttpGet("login")]
    public async Task<IActionResult> Login(
        [FromQuery] string? returnUrl = null,
        [FromQuery] string? bindingIntent = null,
        CancellationToken cancellationToken = default)
    {
        if (!_options.Enabled)
            return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);

        var safeReturnUrl = NormalizeReturnUrl(returnUrl);
        var properties = new AuthenticationProperties();
        if (!string.IsNullOrWhiteSpace(bindingIntent))
        {
            var targetUserId = await cache.GetStringAsync(
                BindingIntentCacheKey(bindingIntent),
                cancellationToken);
            if (targetUserId is null)
                return RedirectToFrontendError("binding_intent_expired", "/account");
            properties.Items[BindingIntentProperty] = bindingIntent;
        }
        var completeUrl = Url.Action(
            nameof(Complete),
            values: new { returnUrl = safeReturnUrl })!;
        properties.RedirectUri = completeUrl;
        try
        {
            await HttpContext.ChallengeAsync(SsoAuthSchemes.Keycloak, properties);
            return new EmptyResult();
        }
        catch (OpenIdConnectProtocolException exception)
        {
            logger.LogWarning(
                exception,
                "Keycloak 拒绝了 OIDC 授权请求。当前回调地址为 {CallbackUrl}",
                EffectiveCallbackUrl());
            return RedirectToFrontendError(
                "configuration_error",
                string.IsNullOrWhiteSpace(bindingIntent) ? "/login" : "/account");
        }
    }

    [AllowAnonymous]
    [ApiExplorerSettings(IgnoreApi = true)]
    [HttpGet("complete")]
    public async Task<ActionResult> Complete(
        [FromQuery] string? returnUrl,
        CancellationToken cancellationToken)
    {
        if (!_options.Enabled)
            return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);

        var authentication = await HttpContext.AuthenticateAsync(
            SsoAuthSchemes.ExternalCookie);
        if (!authentication.Succeeded || authentication.Principal is null)
            return RedirectToFrontendError("authentication_failed");

        var principal = authentication.Principal;
        var subject = principal.FindFirstValue("sub") ??
                      principal.FindFirstValue(ClaimTypes.NameIdentifier);
        if (string.IsNullOrWhiteSpace(subject))
            return RedirectToFrontendError("missing_subject");

        ApplicationUser? user = null;
        var bindingIntent =
            authentication.Properties is { } authenticationProperties &&
            authenticationProperties.Items.TryGetValue(
                BindingIntentProperty,
                out var storedBindingIntent)
                ? storedBindingIntent
                : null;
        if (!string.IsNullOrWhiteSpace(bindingIntent))
        {
            var targetUserId = await cache.GetStringAsync(
                BindingIntentCacheKey(bindingIntent),
                cancellationToken);
            user = targetUserId is null
                ? null
                : await userManager.FindByIdAsync(targetUserId);
            if (user is null)
                return RedirectToFrontendError("binding_intent_expired", "/account");
            if (!user.IsEnabled || await userManager.IsLockedOutAsync(user))
                return RedirectToFrontendError("account_disabled", "/account");

            var linkError = await LinkSsoIdentityAsync(user, subject);
            if (linkError is not null)
                return RedirectToFrontendError(linkError, "/account");
            await cache.RemoveAsync(
                BindingIntentCacheKey(bindingIntent),
                cancellationToken);
        }

        user ??= await userManager.FindByLoginAsync(
            SsoAuthSchemes.LoginProvider,
            subject);
        if (user is null && _options.LinkExistingUsersByUserName)
        {
            var userName = principal.FindFirstValue(_options.UserNameClaim)?.Trim();
            if (!string.IsNullOrWhiteSpace(userName))
            {
                user = await userManager.FindByNameAsync(userName);
                if (user is not null)
                {
                    var linkError = await LinkSsoIdentityAsync(user, subject);
                    if (linkError is not null)
                        return RedirectToFrontendError(linkError);
                }
            }
        }

        if (user is null)
        {
            var bindingCode = WebEncoders.Base64UrlEncode(
                RandomNumberGenerator.GetBytes(32));
            var externalUserName =
                principal.FindFirstValue(_options.UserNameClaim)?.Trim() ??
                principal.FindFirstValue("name")?.Trim() ??
                subject;
            await cache.SetStringAsync(
                BindingCacheKey(bindingCode),
                JsonSerializer.Serialize(new SsoBindingTicket(subject, externalUserName)),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
                },
                cancellationToken);
            await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);

            var bindingPage = BuildFrontendUrl("/sso/bind") +
                $"?code={Uri.EscapeDataString(bindingCode)}" +
                $"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
            return Redirect(bindingPage);
        }
        if (!user.IsEnabled || await userManager.IsLockedOutAsync(user))
            return RedirectToFrontendError("account_disabled");

        user.LastLoginAt = DateTime.UtcNow;
        var updateResult = await userManager.UpdateAsync(user);
        if (!updateResult.Succeeded)
            return RedirectToFrontendError("account_update_failed");

        var exchangeCode = WebEncoders.Base64UrlEncode(
            RandomNumberGenerator.GetBytes(32));
        await cache.SetStringAsync(
            ExchangeCacheKey(exchangeCode),
            user.Id.ToString("D"),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
            },
            cancellationToken);
        await HttpContext.SignOutAsync(SsoAuthSchemes.ExternalCookie);

        var callback = BuildFrontendUrl("/sso/callback") +
            $"?code={Uri.EscapeDataString(exchangeCode)}" +
            $"&redirect={Uri.EscapeDataString(NormalizeReturnUrl(returnUrl))}";
        return Redirect(callback);
    }

    [AllowAnonymous]
    [EnableRateLimiting("public-auth")]
    [HttpPost("exchange")]
    public async Task<ActionResult<LoginResponse>> Exchange(
        SsoExchangeRequest request,
        CancellationToken cancellationToken)
    {
        if (!_options.Enabled)
            return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);

        var cacheKey = ExchangeCacheKey(request.Code);
        var userId = await cache.GetStringAsync(cacheKey, cancellationToken);
        if (userId is null)
            return SsoProblem(
                "统一身份认证结果已失效，请重新登录。",
                StatusCodes.Status401Unauthorized);

        await cache.RemoveAsync(cacheKey, cancellationToken);
        var user = await userManager.FindByIdAsync(userId);
        if (user is null || !user.IsEnabled || await userManager.IsLockedOutAsync(user))
            return SsoProblem(
                "本地账号不存在、已停用或已锁定。",
                StatusCodes.Status401Unauthorized);

        var roles = await userManager.GetRolesAsync(user);
        var session = await authSessionService.CreateAsync(
            user,
            roles,
            request.IsNativeApp
                ? AuthenticationClientType.App
                : AuthenticationClientType.Web,
            cancellationToken);
        return AuthController.CreateLoginResponse(session);
    }

    [AllowAnonymous]
    [EnableRateLimiting("public-auth")]
    [HttpGet("binding")]
    public async Task<ActionResult<SsoBindingInfoResponse>> BindingInfo(
        [FromQuery, Required, MinLength(20), MaxLength(200)] string code,
        CancellationToken cancellationToken)
    {
        if (!_options.Enabled)
            return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);

        var ticket = await ReadBindingTicketAsync(code, cancellationToken);
        if (ticket is null)
            return SsoProblem(
                "账户绑定请求已失效，请重新使用统一身份认证登录。",
                StatusCodes.Status401Unauthorized);

        return new SsoBindingInfoResponse(_options.DisplayName, ticket.ExternalUserName);
    }

    [AllowAnonymous]
    [EnableRateLimiting("public-auth")]
    [HttpPost("bind")]
    public async Task<ActionResult<LoginResponse>> Bind(
        SsoBindRequest request,
        CancellationToken cancellationToken)
    {
        if (!_options.Enabled)
            return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);

        var ticket = await ReadBindingTicketAsync(request.Code, cancellationToken);
        if (ticket is null)
            return SsoProblem(
                "账户绑定请求已失效，请重新使用统一身份认证登录。",
                StatusCodes.Status401Unauthorized);

        var user = await userManager.FindByNameAsync(request.UserName.Trim());
        if (user is null || !user.IsEnabled || await userManager.IsLockedOutAsync(user))
            return InvalidLocalCredentials();

        if (!await userManager.CheckPasswordAsync(user, request.Password))
        {
            await userManager.AccessFailedAsync(user);
            return InvalidLocalCredentials();
        }

        var subjectOwner = await userManager.FindByLoginAsync(
            SsoAuthSchemes.LoginProvider,
            ticket.Subject);
        if (subjectOwner is not null && subjectOwner.Id != user.Id)
            return BindingConflict("该统一身份账号已绑定其他教务系统账号。");

        var keycloakLogins = (await userManager.GetLoginsAsync(user))
            .Where(x => x.LoginProvider == SsoAuthSchemes.LoginProvider)
            .ToList();
        if (keycloakLogins.Any(x => x.ProviderKey != ticket.Subject))
            return BindingConflict("该教务系统账号已绑定其他统一身份账号。");

        if (subjectOwner is null)
        {
            var linkResult = await userManager.AddLoginAsync(
                user,
                new UserLoginInfo(
                    SsoAuthSchemes.LoginProvider,
                    ticket.Subject,
                    _options.DisplayName));
            if (!linkResult.Succeeded)
            {
                subjectOwner = await userManager.FindByLoginAsync(
                    SsoAuthSchemes.LoginProvider,
                    ticket.Subject);
                if (subjectOwner?.Id != user.Id)
                    return BindingConflict("账户绑定失败，请重新发起统一身份认证。");
            }
        }

        await userManager.ResetAccessFailedCountAsync(user);
        user.LastLoginAt = DateTime.UtcNow;
        var updateResult = await userManager.UpdateAsync(user);
        if (!updateResult.Succeeded)
            return SsoProblem("本地账号状态更新失败，请稍后重试。", StatusCodes.Status500InternalServerError);

        await cache.RemoveAsync(BindingCacheKey(request.Code), cancellationToken);
        var roles = await userManager.GetRolesAsync(user);
        var session = await authSessionService.CreateAsync(
            user,
            roles,
            request.IsNativeApp
                ? AuthenticationClientType.App
                : AuthenticationClientType.Web,
            cancellationToken);
        return AuthController.CreateLoginResponse(session);
    }

    [Authorize]
    [HttpGet("account")]
    public async Task<ActionResult<SsoAccountResponse>> Account()
    {
        var user = await CurrentUserAsync();
        if (user is null)
            return Unauthorized();

        var login = (await userManager.GetLoginsAsync(user))
            .SingleOrDefault(x => x.LoginProvider == SsoAuthSchemes.LoginProvider);
        return new SsoAccountResponse(
            _options.Enabled,
            _options.DisplayName,
            login is not null,
            EffectiveCallbackUrl());
    }

    [Authorize]
    [HttpPost("prepare-binding")]
    public async Task<ActionResult<SsoBindingStartResponse>> PrepareBinding(
        CancellationToken cancellationToken)
    {
        if (!_options.Enabled)
            return SsoProblem("统一身份认证尚未启用。", StatusCodes.Status404NotFound);

        var user = await CurrentUserAsync();
        if (user is null || !user.IsEnabled || await userManager.IsLockedOutAsync(user))
            return Unauthorized();
        if ((await userManager.GetLoginsAsync(user))
            .Any(x => x.LoginProvider == SsoAuthSchemes.LoginProvider))
        {
            return BindingConflict("当前账号已绑定统一身份账号，请先解绑后再更换绑定。");
        }

        var intentCode = WebEncoders.Base64UrlEncode(
            RandomNumberGenerator.GetBytes(32));
        await cache.SetStringAsync(
            BindingIntentCacheKey(intentCode),
            user.Id.ToString("D"),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            },
            cancellationToken);
        var loginUrl = Url.Action(
            nameof(Login),
            values: new
            {
                returnUrl = "/account",
                bindingIntent = intentCode
            })!;
        return new SsoBindingStartResponse(loginUrl);
    }

    [Authorize]
    [EnableRateLimiting("public-auth")]
    [HttpPost("unbind")]
    public async Task<IActionResult> Unbind(SsoUnbindRequest request)
    {
        var user = await CurrentUserAsync();
        if (user is null || !user.IsEnabled || await userManager.IsLockedOutAsync(user))
            return Unauthorized();
        if (!await userManager.HasPasswordAsync(user))
            return BindingConflict("当前账号没有本地密码，不能自行解绑，请联系管理员处理。");
        if (!await userManager.CheckPasswordAsync(user, request.Password))
        {
            await userManager.AccessFailedAsync(user);
            return InvalidLocalCredentials();
        }

        var login = (await userManager.GetLoginsAsync(user))
            .SingleOrDefault(x => x.LoginProvider == SsoAuthSchemes.LoginProvider);
        if (login is null)
            return NoContent();

        var result = await userManager.RemoveLoginAsync(
            user,
            login.LoginProvider,
            login.ProviderKey);
        if (!result.Succeeded)
            return SsoProblem("解除统一身份绑定失败，请稍后重试。", StatusCodes.Status500InternalServerError);

        await userManager.ResetAccessFailedCountAsync(user);
        return NoContent();
    }

    internal static string NormalizeReturnUrl(string? returnUrl) =>
        !string.IsNullOrWhiteSpace(returnUrl) &&
        returnUrl.StartsWith('/') &&
        !returnUrl.StartsWith("//", StringComparison.Ordinal)
            ? returnUrl
            : "/dashboard";

    private string BuildFrontendUrl(string path) =>
        string.IsNullOrWhiteSpace(_options.FrontendBaseUrl)
            ? path
            : _options.FrontendBaseUrl.TrimEnd('/') + path;

    private RedirectResult RedirectToFrontendError(
        string error,
        string path = "/login") =>
        Redirect(BuildFrontendUrl(path) +
                 $"?ssoError={Uri.EscapeDataString(error)}");

    private static string ExchangeCacheKey(string code) => $"sso:exchange:{code}";

    private static string BindingCacheKey(string code) => $"sso:binding:{code}";

    private static string BindingIntentCacheKey(string code) =>
        $"sso:binding-intent:{code}";

    private async Task<ApplicationUser?> CurrentUserAsync()
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        return userId is null ? null : await userManager.FindByIdAsync(userId);
    }

    private async Task<string?> LinkSsoIdentityAsync(
        ApplicationUser user,
        string subject)
    {
        var subjectOwner = await userManager.FindByLoginAsync(
            SsoAuthSchemes.LoginProvider,
            subject);
        if (subjectOwner is not null)
            return subjectOwner.Id == user.Id ? null : "identity_already_bound";

        if ((await userManager.GetLoginsAsync(user)).Any(x =>
                x.LoginProvider == SsoAuthSchemes.LoginProvider &&
                x.ProviderKey != subject))
        {
            return "account_already_bound";
        }

        var result = await userManager.AddLoginAsync(
            user,
            new UserLoginInfo(
                SsoAuthSchemes.LoginProvider,
                subject,
                _options.DisplayName));
        if (result.Succeeded)
            return null;

        subjectOwner = await userManager.FindByLoginAsync(
            SsoAuthSchemes.LoginProvider,
            subject);
        return subjectOwner?.Id == user.Id ? null : "account_link_failed";
    }

    private string EffectiveCallbackUrl()
    {
        if (!string.IsNullOrWhiteSpace(_options.CallbackUrl))
            return _options.CallbackUrl;
        return $"{Request.Scheme}://{Request.Host}{Request.PathBase}/signin-keycloak";
    }

    private async Task<SsoBindingTicket?> ReadBindingTicketAsync(
        string code,
        CancellationToken cancellationToken)
    {
        var json = await cache.GetStringAsync(BindingCacheKey(code), cancellationToken);
        if (json is null)
            return null;
        try
        {
            return JsonSerializer.Deserialize<SsoBindingTicket>(json);
        }
        catch (JsonException)
        {
            return null;
        }
    }

    private UnauthorizedObjectResult InvalidLocalCredentials() =>
        Unauthorized(new ProblemDetails
        {
            Title = "账户绑定失败",
            Detail = "教务系统账号或密码不正确，或账号已停用。",
            Status = StatusCodes.Status401Unauthorized
        });

    private ObjectResult BindingConflict(string detail) =>
        SsoProblem(detail, StatusCodes.Status409Conflict);

    private ObjectResult SsoProblem(string detail, int status) =>
        StatusCode(status, new ProblemDetails
        {
            Title = "统一身份认证失败",
            Detail = detail,
            Status = status
        });
}

public sealed record SsoSettingsResponse(
    bool Enabled,
    string DisplayName,
    string CallbackUrl);

public sealed record SsoExchangeRequest(
    [Required, MinLength(20), MaxLength(200)] string Code,
    bool IsNativeApp = false);

public sealed record SsoBindingInfoResponse(
    string ProviderDisplayName,
    string ExternalUserName);

public sealed record SsoBindRequest(
    [Required, MinLength(20), MaxLength(200)] string Code,
    [Required, MaxLength(100)] string UserName,
    [Required, MaxLength(100)] string Password,
    bool IsNativeApp = false);

internal sealed record SsoBindingTicket(string Subject, string ExternalUserName);

public sealed record SsoAccountResponse(
    bool Enabled,
    string ProviderDisplayName,
    bool IsBound,
    string CallbackUrl);

public sealed record SsoBindingStartResponse(string LoginUrl);

public sealed record SsoUnbindRequest(
    [Required, MaxLength(100)] string Password);
