using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using Tiku.Api.Options;
using Tiku.Api.Security;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Domain.Tenancy;

namespace Tiku.Api.Configuration;

internal static class AuthenticationExtensions
{
    internal static IServiceCollection AddApiAuthenticationAndAuthorization(
        this IServiceCollection services,
        IConfiguration configuration,
        IHostEnvironment environment)
    {
        services.AddOptions<JwtOptions>()
            .Bind(configuration.GetSection("Security:Jwt"))
            .ValidateDataAnnotations()
            .Validate(
                options => OptionsValidation.BeValidJwtOptions(options, environment.IsProduction()),
                "Production JWT signing key must be explicitly configured and cannot use the development key.")
            .ValidateOnStart();
        var jwtOptions = configuration
            .GetSection("Security:Jwt")
            .Get<JwtOptions>() ?? new JwtOptions();
        services.AddOptions<BrowserAuthOptions>()
            .Bind(configuration.GetSection(BrowserAuthOptions.SectionName))
            .Validate(
                options => options.AllowedOrigins.All(origin =>
                    Uri.TryCreate(origin, UriKind.Absolute, out var uri) &&
                    (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) &&
                    string.IsNullOrEmpty(uri.PathAndQuery.Trim('/'))),
                "BrowserAuth AllowedOrigins must contain only HTTP(S) origins without paths.")
            .ValidateOnStart();

        services
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options => ConfigureJwtBearer(options, jwtOptions));

        services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
            .Configure<IJwtKeyRing>((options, keyRing) =>
            {
                options.TokenValidationParameters.IssuerSigningKeys = keyRing.ValidationKeys;
                options.TokenValidationParameters.TryAllIssuerSigningKeys = false;
                options.TokenValidationParameters.IssuerSigningKeyResolver = (_, _, kid, _) =>
                    string.IsNullOrWhiteSpace(kid)
                        ? []
                        : keyRing.ValidationKeys.Where(key =>
                            string.Equals(key.KeyId, kid, StringComparison.Ordinal));
            });

        services.AddAuthorization(options =>
        {
            options.FallbackPolicy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.AddPolicy(
                TikuPolicies.AuthenticatedUser,
                policy => policy.RequireAuthenticatedUser());
            options.AddPolicy(
                TikuPolicies.TenantAdmin,
                policy => policy.RequireAuthenticatedUser());
        });
        services.AddTikuRbacAuthorization();
        services.AddSingleton<IAuthorizationMiddlewareResultHandler,
            AuditingAuthorizationMiddlewareResultHandler>();

        return services;
    }

    private static void ConfigureJwtBearer(JwtBearerOptions options, JwtOptions jwtOptions)
    {
        options.MapInboundClaims = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = jwtOptions.Issuer,
            ValidateAudience = true,
            ValidAudience = jwtOptions.Audience,
            ValidateIssuerSigningKey = true,
            RequireSignedTokens = true,
            ValidAlgorithms = [SecurityAlgorithms.RsaSha256],
            ValidateLifetime = true,
            RequireExpirationTime = true,
            ClockSkew = TimeSpan.FromMinutes(1),
            NameClaimType = TikuClaimTypes.UserId
        };
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                if (string.IsNullOrWhiteSpace(context.Request.Headers.Authorization) &&
                    IsSameOriginBrowserRequest(context.Request) &&
                    context.Request.Cookies.TryGetValue(BrowserAuthOptions.AccessCookie, out var accessToken))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            },
            OnTokenValidated = ValidateTokenAsync,
            OnChallenge = WriteTenantConflictChallengeAsync
        };
    }

    private static bool IsSameOriginBrowserRequest(HttpRequest request)
    {
        var source = request.Headers.Origin.ToString();
        if (string.IsNullOrWhiteSpace(source))
        {
            source = request.Headers.Referer.ToString();
        }

        if (Uri.TryCreate(source, UriKind.Absolute, out var uri))
        {
            return string.Equals(uri.Scheme, request.Scheme, StringComparison.OrdinalIgnoreCase) &&
                   string.Equals(uri.Authority, request.Host.Value, StringComparison.OrdinalIgnoreCase);
        }

        return string.Equals(
            request.Headers["Sec-Fetch-Site"].ToString(),
            "same-origin",
            StringComparison.OrdinalIgnoreCase);
    }

    private static async Task ValidateTokenAsync(TokenValidatedContext context)
    {
        var principal = context.Principal;
        if (!Guid.TryParse(principal?.FindFirst(TikuClaimTypes.UserId)?.Value, out var userId) ||
            !Guid.TryParse(principal?.FindFirst(TikuClaimTypes.SessionId)?.Value, out var sessionId) ||
            string.IsNullOrWhiteSpace(principal?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value) ||
            !long.TryParse(
                principal?.FindFirst(JwtRegisteredClaimNames.Iat)?.Value,
                NumberStyles.None,
                CultureInfo.InvariantCulture,
                out _))
        {
            context.Fail("Missing or invalid subject/session/jti/iat claim.");
            return;
        }

        var realmValue = principal.FindFirst(TikuClaimTypes.Realm)?.Value;
        var realm = string.Equals(realmValue, "tenant", StringComparison.Ordinal)
            ? AuthRealm.Tenant
            : string.Equals(realmValue, "platform", StringComparison.Ordinal)
                ? AuthRealm.Platform
                : (AuthRealm?)null;
        var tenantIdValue = principal.FindFirst(TikuClaimTypes.TenantId)?.Value;
        var tenantId = Guid.TryParse(tenantIdValue, out var parsedTenantId)
            ? parsedTenantId
            : (Guid?)null;
        if (realm is null || (realm == AuthRealm.Tenant) != tenantId.HasValue)
        {
            context.Fail("Token scope and tenant claims are inconsistent.");
            return;
        }

        var resolutionOptions = context.HttpContext.RequestServices
            .GetRequiredService<Microsoft.Extensions.Options.IOptions<TenantResolutionOptions>>().Value;
        var requestHost = context.HttpContext.Request.Host.Host.Trim().TrimEnd('.');
        var isPlatformHost = resolutionOptions.PlatformHosts.Any(host =>
            string.Equals(host.Trim().TrimEnd('.'), requestHost, StringComparison.OrdinalIgnoreCase));
        var resolvedTenantContext = context.HttpContext.RequestServices.GetRequiredService<ITenantContext>();
        var tenantInitializer = context.HttpContext.RequestServices
            .GetRequiredService<ITenantContextInitializer>();

        if (realm == AuthRealm.Platform)
        {
            if (!isPlatformHost || resolvedTenantContext.IsResolved)
            {
                context.HttpContext.Items["tenant_context_conflict"] = true;
                context.Fail("Platform tokens are only valid on a platform host.");
                return;
            }
        }
        else
        {
            if (isPlatformHost && !resolvedTenantContext.IsResolved)
            {
                context.HttpContext.Items["tenant_context_conflict"] = true;
                context.Fail("Tenant tokens on a platform host require a matching tenant code.");
                return;
            }

            try
            {
                tenantInitializer.Initialize(tenantId!.Value, null, TenantResolutionSource.Jwt);
            }
            catch (TenantContextConflictException)
            {
                context.HttpContext.Items["tenant_context_conflict"] = true;
                context.Fail("Authenticated tenant does not match the request host.");
                return;
            }
        }

        var sessionStore = context.HttpContext.RequestServices.GetRequiredService<IAuthSessionStore>();
        var session = await sessionStore.ValidateAccessSessionAsync(
            sessionId,
            userId,
            realm.Value,
            tenantId,
            context.HttpContext.RequestAborted);
        if (session is null)
        {
            context.Fail("Session, identity, membership, tenant or role state is no longer valid.");
            return;
        }

        context.HttpContext.RequestServices.GetRequiredService<IRequestSecurityState>()
            .SetValidatedSession(session);
    }

    private static async Task WriteTenantConflictChallengeAsync(JwtBearerChallengeContext context)
    {
        if (!context.HttpContext.Items.ContainsKey("tenant_context_conflict"))
        {
            return;
        }

        context.HandleResponse();
        context.Response.StatusCode = StatusCodes.Status403Forbidden;
        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Title = "Authenticated tenant does not match the request host.",
            Status = StatusCodes.Status403Forbidden,
            Extensions = { ["code"] = "tenant_context_conflict" }
        });
    }
}
