using System.Security.Claims;
using System.Globalization;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.RateLimiting;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using MemberCenter.Infrastructure.Configuration;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using MemberCenter.Infrastructure.Services;
using MemberCenter.Web.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

EnvLoader.LoadDotEnvIfDevelopment();

var builder = WebApplication.CreateBuilder(args);
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();
SecurityRuntimeOptionsValidator.Validate(identitySecurity);
SecurityRuntimeOptionsValidator.Validate(rateLimits);
SecurityRuntimeOptionsValidator.Validate(newsletterTokens);
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 dataProtectionCertificate = CertificateLoader.LoadFromConfiguration(
    builder.Configuration, "DataProtection:Certificate", required: !builder.Environment.IsDevelopment());
var previousDataProtectionCertificates = CertificateLoader.LoadPreviousFromConfiguration(
    builder.Configuration, "DataProtection:Certificate");

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.Configure<SecurityStampValidatorOptions>(options =>
{
    options.ValidationInterval = TimeSpan.Zero;
});

var googleClientId = builder.Configuration["Authentication:Google:ClientId"]
    ?? Environment.GetEnvironmentVariable("Authentication__Google__ClientId");
var googleClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]
    ?? Environment.GetEnvironmentVariable("Authentication__Google__ClientSecret");

var authenticationBuilder = builder.Services.AddAuthentication();
if (!string.IsNullOrWhiteSpace(googleClientId) && !string.IsNullOrWhiteSpace(googleClientSecret))
{
    authenticationBuilder.AddGoogle(options =>
    {
        options.ClientId = googleClientId;
        options.ClientSecret = googleClientSecret;
    });
}

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

    options.Events = new CookieAuthenticationEvents
    {
        OnRedirectToLogin = context => HandleAdminAuthRedirectAsync(context),
        OnRedirectToAccessDenied = context => HandleAdminAuthRedirectAsync(context),
        OnValidatePrincipal = context => ValidatePrincipalAsync(context)
    };
});

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Admin", policy => policy.RequireRole("admin", "superuser"));
    options.AddPolicy("Superuser", policy => policy.RequireRole("superuser"));
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

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.PublicAuthLogin, context =>
        CreateFixedWindowLimiter(context, "web-auth-login", rateLimits.Web.AuthLogin));

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

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

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

builder.Services.AddScoped<INewsletterService, NewsletterService>();
builder.Services.AddScoped<IEmailBlacklistService, EmailBlacklistService>();
builder.Services.AddScoped<ITenantService, TenantService>();
builder.Services.AddScoped<ITenantManagerService, TenantManagerService>();
builder.Services.AddScoped<INewsletterListService, NewsletterListService>();
builder.Services.AddScoped<IAuditLogService, AuditLogService>();
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<ISecuritySettingsService, SecuritySettingsService>();
builder.Services.AddScoped<ISubscriptionAdminService, SubscriptionAdminService>();
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.Configure<NewsletterTokenOptions>(builder.Configuration.GetSection("NewsletterTokens"));
builder.Services.Configure<SendEngineWebhookOptions>(builder.Configuration.GetSection("SendEngine"));
builder.Services.AddHttpClient<SendEngineWebhookPublisher>();
builder.Services.AddScoped<ISendEngineWebhookPublisher, SendEngineWebhookPublisher>();
builder.Services.AddScoped<AdminDashboardFactory>();
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        options.UseEntityFrameworkCore()
            .UseDbContext<MemberCenterDbContext>();
    });

builder.Services.AddControllersWithViews(options =>
{
    options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
    options.Filters.Add(new Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute());
})
    .AddViewLocalization()
    .AddDataAnnotationsLocalization();
builder.Services.AddHttpContextAccessor();

var supportedCultures = new[]
{
    new CultureInfo("en-US"),
    new CultureInfo("zh-TW")
};
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    options.DefaultRequestCulture = new RequestCulture("en-US");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
    options.RequestCultureProviders = new RequestCultureProvider[]
    {
        new CookieRequestCultureProvider(),
        new AcceptLanguageHeaderRequestCultureProvider()
    };
});

var app = builder.Build();
CertificateLoader.LogExpirationWarning(
    app.Logger,
    "Data Protection",
    dataProtectionCertificate,
    builder.Configuration.GetValue("Certificates:ExpirationWarningDays", 30));

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

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
}

app.UseForwardedHeaders();
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        var headers = context.Response.Headers;
        headers.TryAdd("Content-Security-Policy",
            "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'");
        headers.TryAdd("X-Content-Type-Options", "nosniff");
        headers.TryAdd("X-Frame-Options", "DENY");
        headers.TryAdd("Referrer-Policy", "no-referrer");
        headers.TryAdd("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()");
        return Task.CompletedTask;
    });

    await next();
});
app.UseStaticFiles();
app.UseRequestLocalization();
app.UseRouting();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "admin",
    pattern: "admin/{controller=Home}/{action=Index}/{id?}",
    defaults: new { area = "Admin" });

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

static Task HandleAdminAuthRedirectAsync(RedirectContext<CookieAuthenticationOptions> context)
{
    if (context.Request.Path.StartsWithSegments("/admin", StringComparison.OrdinalIgnoreCase))
    {
        context.Response.StatusCode = StatusCodes.Status404NotFound;
        return Task.CompletedTask;
    }

    context.Response.Redirect(context.RedirectUri);
    return Task.CompletedTask;
}

static async Task ValidatePrincipalAsync(CookieValidatePrincipalContext context)
{
    await SecurityStampValidator.ValidatePrincipalAsync(context);
    if (context.Principal?.Identity?.IsAuthenticated != true)
    {
        return;
    }

    var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
    if (!Guid.TryParse(userId, out var parsedUserId))
    {
        return;
    }

    var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
    var user = await userManager.FindByIdAsync(parsedUserId.ToString());
    if (user is null || user.DisabledAt.HasValue)
    {
        context.RejectPrincipal();
        await context.HttpContext.SignOutAsync();
    }
}

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