using System.Security.Claims;
using System.Threading.RateLimiting;
using MemberCenter.Infrastructure.Configuration;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using MemberCenter.Infrastructure.Services;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore;

EnvLoader.LoadDotEnvIfDevelopment();

var builder = WebApplication.CreateBuilder(args);
var pathBase = NormalizePathBase(builder.Configuration["PathBase"]);
var issuer = builder.Configuration["Auth:Issuer"];
var issuerUri = ParseAbsoluteUriOrThrow(issuer, "Auth:Issuer");
var allowInternalHttpTokenEndpoint = builder.Configuration.GetValue("Auth:AllowInternalHttpTokenEndpoint", false);
var identitySecurity = builder.Configuration.GetSection("IdentitySecurity").Get<IdentitySecurityOptions>() ?? new();
var rateLimits = builder.Configuration.GetSection("RateLimits").Get<RateLimitSettings>() ?? new();
var newsletterTokens = builder.Configuration.GetSection("NewsletterTokens").Get<NewsletterTokenOptions>() ?? new();
var fileAccessTokens = builder.Configuration.GetSection("FileAccessTokens").Get<FileAccessTokenOptions>() ?? new();
SecurityRuntimeOptionsValidator.Validate(identitySecurity);
SecurityRuntimeOptionsValidator.Validate(rateLimits);
SecurityRuntimeOptionsValidator.Validate(newsletterTokens);
SecurityRuntimeOptionsValidator.Validate(fileAccessTokens);
var connectionString = builder.Configuration.GetConnectionString("Default");
if (string.IsNullOrWhiteSpace(connectionString))
{
    if (!builder.Environment.IsDevelopment())
    {
        throw new InvalidOperationException("ConnectionStrings:Default is required outside Development.");
    }

    connectionString = "Host=localhost;Database=member_center;Username=postgres;Password=postgres";
}
var requireProductionCertificates = !builder.Environment.IsDevelopment();
var signingCertificate = CertificateLoader.LoadFromConfiguration(
    builder.Configuration, "Auth:Certificates:Signing", requireProductionCertificates);
var previousSigningCertificates = CertificateLoader.LoadPreviousFromConfiguration(
    builder.Configuration, "Auth:Certificates:Signing");
var encryptionCertificate = CertificateLoader.LoadFromConfiguration(
    builder.Configuration, "Auth:Certificates:Encryption", requireProductionCertificates);
var previousEncryptionCertificates = CertificateLoader.LoadPreviousFromConfiguration(
    builder.Configuration, "Auth:Certificates:Encryption");
var dataProtectionCertificate = CertificateLoader.LoadFromConfiguration(
    builder.Configuration, "DataProtection:Certificate", requireProductionCertificates);
var previousDataProtectionCertificates = CertificateLoader.LoadPreviousFromConfiguration(
    builder.Configuration, "DataProtection:Certificate");
if (!builder.Environment.IsDevelopment() && issuerUri is null)
{
    throw new InvalidOperationException("Auth:Issuer is required outside Development.");
}
if (!builder.Environment.IsDevelopment() && issuerUri?.Scheme != Uri.UriSchemeHttps)
{
    throw new InvalidOperationException("Auth:Issuer must use HTTPS outside Development.");
}

var dataProtectionBuilder = builder.Services.AddDataProtection()
    .SetApplicationName("MemberCenter")
    .PersistKeysToDbContext<MemberCenterDbContext>();
if (dataProtectionCertificate is not null)
{
    dataProtectionBuilder.ProtectKeysWithCertificate(dataProtectionCertificate);
    if (previousDataProtectionCertificates.Count > 0)
    {
        dataProtectionBuilder.UnprotectKeysWithAnyCertificate(
            new[] { dataProtectionCertificate }.Concat(previousDataProtectionCertificates).ToArray());
    }
}

builder.Services.AddDbContext<MemberCenterDbContext>(options =>
{
    options.UseNpgsql(connectionString);
    options.UseOpenIddict();
});

builder.Services
    .AddIdentity<ApplicationUser, ApplicationRole>(options =>
    {
        options.User.RequireUniqueEmail = true;
        options.Password.RequireDigit = identitySecurity.Password.RequireDigit;
        options.Password.RequireLowercase = identitySecurity.Password.RequireLowercase;
        options.Password.RequireUppercase = identitySecurity.Password.RequireUppercase;
        options.Password.RequireNonAlphanumeric = identitySecurity.Password.RequireNonAlphanumeric;
        options.Password.RequiredLength = identitySecurity.Password.RequiredLength;
        options.Lockout.AllowedForNewUsers = identitySecurity.Lockout.AllowedForNewUsers;
        options.Lockout.MaxFailedAccessAttempts = identitySecurity.Lockout.MaxFailedAccessAttempts;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(identitySecurity.Lockout.DefaultLockoutMinutes);
    })
    .AddEntityFrameworkStores<MemberCenterDbContext>()
    .AddDefaultTokenProviders();

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Path = "/";
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        options.UseEntityFrameworkCore()
            .UseDbContext<MemberCenterDbContext>();
    })
    .AddServer(options =>
    {
        options.SetAuthorizationEndpointUris(WithPathBase(pathBase, "/oauth/authorize"));
        options.SetTokenEndpointUris(WithPathBase(pathBase, "/oauth/token"));
        options.SetLogoutEndpointUris(WithPathBase(pathBase, "/auth/logout"));
        if (issuerUri is not null)
        {
            options.SetIssuer(issuerUri);
        }

        options.AllowAuthorizationCodeFlow()
            .RequireProofKeyForCodeExchange();
        options.AllowRefreshTokenFlow();
        options.AllowClientCredentialsFlow();

        options.RegisterScopes(
            OpenIddictConstants.Scopes.OpenId,
            OpenIddictConstants.Scopes.Email,
            OpenIddictConstants.Scopes.Profile,
            "profile:basic.read",
            "profile:basic.write",
            "profile:addresses.read",
            "profile:addresses.write",
            "profile:subscriptions.read",
            "profile:subscriptions.write",
            "newsletter:list.read",
            "newsletter:list.read.global",
            "newsletter:subscriptions.read",
            "newsletter:subscriptions.write",
            "newsletter:send.write",
            "newsletter:send.read",
            "newsletter:events.read",
            "newsletter:events.write",
            "newsletter:events.write.global",
            "files:upload.write",
            "files:download.read",
            "files:download.delegate",
            "files:metadata.read",
            "files:delete");

        if (signingCertificate is not null && encryptionCertificate is not null)
        {
            foreach (var certificate in previousSigningCertificates)
            {
                options.AddSigningCertificate(certificate);
            }
            foreach (var certificate in previousEncryptionCertificates)
            {
                options.AddEncryptionCertificate(certificate);
            }
            options.AddSigningCertificate(signingCertificate);
            options.AddEncryptionCertificate(encryptionCertificate);
        }
        else
        {
            options.AddDevelopmentEncryptionCertificate();
            options.AddDevelopmentSigningCertificate();
        }
        options.DisableAccessTokenEncryption();

        var aspNetCore = options.UseAspNetCore()
            .EnableAuthorizationEndpointPassthrough()
            .EnableTokenEndpointPassthrough()
            .EnableLogoutEndpointPassthrough()
            .EnableStatusCodePagesIntegration();

        if (builder.Environment.IsDevelopment() || allowInternalHttpTokenEndpoint)
        {
            // Allows direct OAuth calls over a VPC-private HTTP endpoint while retaining an HTTPS issuer.
            aspNetCore.DisableTransportSecurityRequirement();
        }
    })
    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Admin", policy => policy.RequireRole("admin", "superuser"));
    options.AddPolicy("Superuser", policy => policy.RequireRole("superuser"));
    options.AddPolicy("ProfileBasicRead", policy => policy.RequireAssertion(context => context.User.HasScope("profile:basic.read")));
    options.AddPolicy("ProfileBasicWrite", policy => policy.RequireAssertion(context => context.User.HasScope("profile:basic.write")));
    options.AddPolicy("ProfileAddressesRead", policy => policy.RequireAssertion(context => context.User.HasScope("profile:addresses.read")));
    options.AddPolicy("ProfileAddressesWrite", policy => policy.RequireAssertion(context => context.User.HasScope("profile:addresses.write")));
    options.AddPolicy("ProfileSubscriptionsRead", policy => policy.RequireAssertion(context => context.User.HasScope("profile:subscriptions.read")));
    options.AddPolicy("ProfileSubscriptionsWrite", policy => policy.RequireAssertion(context => context.User.HasScope("profile:subscriptions.write")));
    options.AddPolicy("FilesDownloadDelegate", policy => policy.RequireAssertion(context => context.User.HasScope("files:download.delegate")));
    options.AddPolicy("FilesDownloadRead", policy => policy.RequireAssertion(context => context.User.HasScope("files:download.read")));
});

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    TrustedForwardedHeaders.Configure(options, builder.Configuration);
});
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
    options.OnRejected = static async (context, token) =>
    {
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter = Math.Ceiling(retryAfter.TotalSeconds).ToString();
        }

        await context.HttpContext.Response.WriteAsync("Too many requests.", token);
    };

    options.AddPolicy(RateLimitPolicyNames.PublicAuthRegister, context =>
        CreateFixedWindowLimiter(context, "api-auth-register", rateLimits.Api.AuthRegister));

    options.AddPolicy(RateLimitPolicyNames.PublicAuthRecovery, context =>
        CreateFixedWindowLimiter(context, "api-auth-recovery", rateLimits.Api.AuthRecovery));

    options.AddPolicy(RateLimitPolicyNames.PublicAuthTokenConsumption, context =>
        CreateFixedWindowLimiter(context, "api-auth-token-consumption", rateLimits.Api.AuthTokenConsumption));

    options.AddPolicy(RateLimitPolicyNames.PublicNewsletterSubscribe, context =>
        CreateFixedWindowLimiter(context, "api-newsletter-subscribe", rateLimits.Api.NewsletterSubscribe));

    options.AddPolicy(RateLimitPolicyNames.PublicNewsletterUnsubscribeToken, context =>
        CreateFixedWindowLimiter(context, "api-newsletter-unsubscribe-token", rateLimits.Api.NewsletterUnsubscribeToken));

    options.AddPolicy(RateLimitPolicyNames.NewsletterOneClickToken, context =>
        CreateFixedWindowLimiter(context, "api-newsletter-one-click-token", rateLimits.Api.NewsletterOneClickToken));

    options.AddPolicy(RateLimitPolicyNames.OAuthToken, context =>
        CreateFixedWindowLimiter(context, "api-oauth-token", rateLimits.Api.OAuthToken));
});

builder.Services.AddControllers();
builder.Services.AddScoped<IAuditLogWriter, AuditLogWriter>();
builder.Services.AddScoped<IAccountGovernanceService, AccountGovernanceService>();
builder.Services.AddScoped<IAccountEmailTemplateService, AccountEmailTemplateService>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IAccountEmailService, AccountEmailService>();
builder.Services.AddScoped<INewsletterService, NewsletterService>();
builder.Services.AddScoped<IEmailBlacklistService, EmailBlacklistService>();
builder.Services.AddScoped<ISecuritySettingsService, SecuritySettingsService>();
builder.Services.AddScoped<ITenantService, TenantService>();
builder.Services.AddScoped<ITenantManagerService, TenantManagerService>();
builder.Services.AddScoped<INewsletterListService, NewsletterListService>();
builder.Services.AddScoped<IAccountProvisioningService, AccountProvisioningService>();
builder.Services.AddScoped<IProfileService, ProfileService>();
builder.Services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
builder.Services.AddScoped<AdminPermissionService>();
builder.Services.AddScoped<IAdminPermissionChecker>(services => services.GetRequiredService<AdminPermissionService>());
builder.Services.AddScoped<IAdminPermissionSeeder>(services => services.GetRequiredService<AdminPermissionService>());
builder.Services.AddHttpContextAccessor();
builder.Services.Configure<NewsletterTokenOptions>(builder.Configuration.GetSection("NewsletterTokens"));
builder.Services.Configure<FileAccessTokenOptions>(builder.Configuration.GetSection("FileAccessTokens"));
builder.Services.Configure<SendEngineWebhookOptions>(builder.Configuration.GetSection("SendEngine"));
builder.Services.AddHttpClient<SendEngineWebhookPublisher>();
builder.Services.AddScoped<ISendEngineWebhookPublisher, SendEngineWebhookPublisher>();

var app = builder.Build();
var certificateWarningDays = builder.Configuration.GetValue("Certificates:ExpirationWarningDays", 30);
CertificateLoader.LogExpirationWarning(app.Logger, "OpenIddict signing", signingCertificate, certificateWarningDays);
CertificateLoader.LogExpirationWarning(app.Logger, "OpenIddict encryption", encryptionCertificate, certificateWarningDays);
CertificateLoader.LogExpirationWarning(app.Logger, "Data Protection", dataProtectionCertificate, certificateWarningDays);

await EnsureAuthRegistryDefaultsAsync(app.Services);
await EnsureAdminPermissionDefaultsAsync(app.Services);

app.UseForwardedHeaders();
if (!string.IsNullOrWhiteSpace(pathBase))
{
    app.UsePathBase(pathBase);
}

app.Use(async (context, next) =>
{
    if (issuerUri is not null && IsOpenIddictRequest(context.Request.Path))
    {
        context.Request.Host = HostString.FromUriComponent(issuerUri);
        if (context.Request.IsHttps || allowInternalHttpTokenEndpoint)
        {
            context.Request.Scheme = issuerUri.Scheme;
        }
    }

    await next();
});

app.UseRouting();
app.UseAuthentication();
app.UseRateLimiter();
app.Use(async (context, next) =>
{
    if (context.User.Identity?.IsAuthenticated == true)
    {
        if (context.User.HasClaim(claim => claim.Type == "client_usage"))
        {
            await next();
            return;
        }

        var subject = context.User.FindFirstValue(OpenIddictConstants.Claims.Subject)
            ?? context.User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (Guid.TryParse(subject, out var userId))
        {
            var userManager = context.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
            var user = await userManager.FindByIdAsync(userId.ToString());
            if (user is null || user.DisabledAt.HasValue)
            {
                context.Response.StatusCode = StatusCodes.Status403Forbidden;
                await context.Response.WriteAsync("Account is disabled.");
                return;
            }

            var tokenSecurityStamp = context.User.FindFirst("AspNet.Identity.SecurityStamp")?.Value;
            if (!string.IsNullOrWhiteSpace(tokenSecurityStamp)
                && !string.Equals(tokenSecurityStamp, user.SecurityStamp, StringComparison.Ordinal))
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                await context.Response.WriteAsync("Session has been invalidated.");
                return;
            }
        }
    }

    await next();
});
app.UseAuthorization();

app.MapControllers();

app.Run();

static string? NormalizePathBase(string? pathBase)
{
    if (string.IsNullOrWhiteSpace(pathBase))
    {
        return null;
    }

    var normalized = pathBase.StartsWith('/') ? pathBase : $"/{pathBase}";
    return normalized.Length > 1 ? normalized.TrimEnd('/') : normalized;
}

static string WithPathBase(string? pathBase, string relativePath)
{
    var normalizedRelativePath = relativePath.StartsWith('/') ? relativePath : $"/{relativePath}";
    return string.IsNullOrWhiteSpace(pathBase)
        ? normalizedRelativePath
        : $"{pathBase}{normalizedRelativePath}";
}

static Uri? ParseAbsoluteUriOrThrow(string? uri, string configKey)
{
    if (string.IsNullOrWhiteSpace(uri))
    {
        return null;
    }

    if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsed))
    {
        throw new InvalidOperationException($"{configKey} must be an absolute URI.");
    }

    return parsed;
}

static bool IsOpenIddictRequest(PathString path)
{
    return path.StartsWithSegments("/.well-known", StringComparison.OrdinalIgnoreCase)
        || path.StartsWithSegments("/oauth", StringComparison.OrdinalIgnoreCase)
        || path.StartsWithSegments("/auth", StringComparison.OrdinalIgnoreCase);
}

static RateLimitPartition<string> CreateFixedWindowLimiter(
    HttpContext context,
    string policyPrefix,
    FixedWindowRateLimitSettings settings)
{
    var identifier = context.User.Identity?.IsAuthenticated == true
        ? context.User.FindFirstValue(ClaimTypes.NameIdentifier)
            ?? context.User.Identity?.Name
            ?? context.Connection.RemoteIpAddress?.ToString()
            ?? "unknown"
        : context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

    var partitionKey = $"{policyPrefix}:{identifier}";
    return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions
    {
        PermitLimit = settings.PermitLimit,
        Window = TimeSpan.FromSeconds(settings.WindowSeconds),
        QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
        QueueLimit = 0,
        AutoReplenishment = true
    });
}

static async Task EnsureAuthRegistryDefaultsAsync(IServiceProvider services)
{
    await using var scope = services.CreateAsyncScope();
    var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
    await registry.EnsureDefaultsAsync();
}

static async Task EnsureAdminPermissionDefaultsAsync(IServiceProvider services)
{
    await using var scope = services.CreateAsyncScope();
    var seeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
    await seeder.EnsureDefaultsAsync();
}
