using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Guestlist.Components;
using Guestlist;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddCascadingAuthenticationState();

builder.Services.Configure<MongoOptions>(
    builder.Configuration.GetSection("Mongo"));

builder.Services.AddSingleton<MongoDbContext>();
builder.Services.AddScoped<SetupStatusService>();

builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<GuestEntryService>();
builder.Services.AddScoped<AccessCodeService>();

builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/login";
        options.AccessDeniedPath = "/login";
        options.SlidingExpiration = true;
        options.ExpireTimeSpan = TimeSpan.FromHours(8);
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
    });

builder.Services.AddAuthorization();

builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.Cookie.Name = ".Guestlist.GuestAccess";
    options.IdleTimeout = TimeSpan.FromHours(8);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});

var app = builder.Build();

app.Use(async (context, next) =>
{
    context.Response.Headers.TryAdd("X-Content-Type-Options", "nosniff");
    context.Response.Headers.TryAdd("X-Frame-Options", "DENY");
    context.Response.Headers.TryAdd("Referrer-Policy", "no-referrer");
    context.Response.Headers.TryAdd("Permissions-Policy", "camera=(), microphone=(), geolocation=()");

    await next();
});

app.MapPost("/auth/login", async (
    HttpContext httpContext,
    UserService userService) =>
{
    var form = await httpContext.Request.ReadFormAsync();

    var username = form["username"].ToString();
    var password = form["password"].ToString();
    var returnUrl = form["returnUrl"].ToString();

    if (username.Length > InputRules.UsernameMaxLength ||
        password.Length > InputRules.PasswordMaxLength ||
        returnUrl.Length > 300)
    {
        return Results.Redirect("/login?error=invalid");
    }

    UserDocument? user;

    try
    {
        user = await userService.ValidateCredentialsAsync(username, password);
    }
    catch
    {
        return Results.Redirect("/login?error=service");
    }

    if (user is null)
    {
        return Results.Redirect("/login?error=invalid");
    }

    var claims = new List<Claim>
    {
        new(ClaimTypes.NameIdentifier, user.Id ?? ""),
        new(ClaimTypes.Name, user.Username),
        new(ClaimTypes.Role, user.Role)
    };

    var identity = new ClaimsIdentity(
        claims,
        CookieAuthenticationDefaults.AuthenticationScheme);

    var principal = new ClaimsPrincipal(identity);

    await httpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        principal);

    var targetUrl = IsLocalReturnUrl(returnUrl)
        ? returnUrl
        : null;

    targetUrl = string.IsNullOrWhiteSpace(targetUrl)
        ? "/guests"
        : targetUrl;

    return Results.Redirect(targetUrl);
});

app.MapPost("/auth/logout", async (HttpContext httpContext) =>
{
    await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    httpContext.Session.Clear();

    return Results.Redirect("/login");
});

app.MapPost("/guest/access", async (
    HttpContext httpContext,
    AccessCodeService accessCodeService) =>
{
    var form = await httpContext.Request.ReadFormAsync();
    var accessCode = form["accessCode"].ToString();

    if (accessCode.Length > InputRules.AccessCodeMaxLength)
    {
        return Results.Redirect("/guest?error=invalid");
    }

    bool isValid;

    try
    {
        isValid = await accessCodeService.ValidateGuestAccessCodeAsync(accessCode);
    }
    catch
    {
        return Results.Redirect("/guest?error=service");
    }

    if (!isValid)
    {
        return Results.Redirect("/guest?error=invalid");
    }

    httpContext.Session.SetString("GuestAccessGranted", "true");

    return Results.Redirect("/list");
});

app.MapPost("/guest/logout", (HttpContext httpContext) =>
{
    httpContext.Session.Remove("GuestAccessGranted");

    return Results.Redirect("/guest");
});

using (var scope = app.Services.CreateScope())
{
    var userService = scope.ServiceProvider.GetRequiredService<UserService>();
    var guestEntryService = scope.ServiceProvider.GetRequiredService<GuestEntryService>();
    var accessCodeService = scope.ServiceProvider.GetRequiredService<AccessCodeService>();

    await userService.EnsureIndexesAsync();
    await guestEntryService.EnsureIndexesAsync();
    await accessCodeService.EnsureIndexesAsync();
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.UseSession();

app.Use(async (context, next) =>
{
    var path = context.Request.Path;
    
    var isStaticFile = System.IO.Path.HasExtension(path.Value);

    var allowedPaths =
        isStaticFile ||
        path.StartsWithSegments("/setup") ||
        path.StartsWithSegments("/auth/logout") ||
        path.StartsWithSegments("/guest/logout") ||
        path.StartsWithSegments("/_framework") ||
        path.StartsWithSegments("/_blazor") ||
        path.StartsWithSegments("/css") ||
        path.StartsWithSegments("/lib") ||
        path.StartsWithSegments("/favicon.png");

    if (allowedPaths)
    {
        await next();
        return;
    }

    using var scope = context.RequestServices.CreateScope();
    var setupStatusService = scope.ServiceProvider.GetRequiredService<SetupStatusService>();
    var userService = scope.ServiceProvider.GetRequiredService<UserService>();

    var isSetupComplete = await setupStatusService.IsSetupCompleteAsync();

    if (!isSetupComplete)
    {
        var hasAnyUsers = await userService.HasAnyUsersAsync();
        var allowedRecoveryPath =
            hasAnyUsers &&
            (path.StartsWithSegments("/login") ||
             path.StartsWithSegments("/auth/login") ||
             path.StartsWithSegments("/auth/logout") ||
             path.StartsWithSegments("/admin"));

        if (allowedRecoveryPath)
        {
            await next();
            return;
        }

        context.Response.Redirect("/setup");
        return;
    }

    await next();
});

app.UseAntiforgery();

app.MapStaticAssets();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

static bool IsLocalReturnUrl(string? returnUrl)
{
    return !string.IsNullOrWhiteSpace(returnUrl) &&
           returnUrl.StartsWith('/') &&
           !returnUrl.StartsWith("//");
}
