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.AddScoped<DatabaseBackupService>();

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(options =>
{
    options.AddPolicy("StaffOnly", policy => policy.RequireRole(UserRoles.Admin, UserRoles.Maintainer));
    options.AddPolicy("AdminOnly", policy => policy.RequireRole(UserRoles.Admin));
});

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

app.MapPost("/setup/complete", async (
    HttpContext httpContext,
    UserService userService,
    AccessCodeService accessCodeService,
    SetupStatusService setupStatusService) =>
{
    var form = await httpContext.Request.ReadFormAsync();

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

    if (await userService.HasAnyUsersAsync())
    {
        return Results.Redirect("/setup?error=existing-user");
    }

    if (string.IsNullOrWhiteSpace(displayName))
    {
        displayName = username;
    }

    var adminCreated = false;

    try
    {
        await userService.CreateAdminAsync(displayName, username, password);
        adminCreated = true;

        await accessCodeService.SetGuestAccessCodeAsync(guestAccessCode);
        await setupStatusService.MarkSetupCompleteAsync();
    }
    catch (InvalidOperationException exception)
    {
        if (adminCreated)
        {
            await userService.DeleteUserByUsernameAsync(username);
        }

        var error = Uri.EscapeDataString(exception.Message);
        return Results.Redirect($"/setup?error={error}");
    }
    catch
    {
        if (adminCreated)
        {
            await userService.DeleteUserByUsernameAsync(username);
        }

        return Results.Redirect("/setup?error=setup-failed");
    }

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

app.MapPost("/setup/restore", async (
    DatabaseBackupService databaseBackupService,
    UserService userService,
    GuestEntryService guestEntryService,
    AccessCodeService accessCodeService,
    SetupStatusService setupStatusService) =>
{
    try
    {
        if (await userService.HasAnyUsersAsync())
        {
            return Results.Redirect("/setup?error=restore-users-exist");
        }

        var restored = await databaseBackupService.RestoreLatestDatabaseBackupAsync();

        if (restored is null)
        {
            return Results.Redirect("/setup?error=restore-failed");
        }

        await setupStatusService.MarkSetupCompleteAsync();
        await userService.EnsureIndexesAsync();
        await guestEntryService.EnsureIndexesAsync();
        await accessCodeService.EnsureIndexesAsync();
    }
    catch
    {
        return Results.Redirect("/setup?error=restore-failed");
    }

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

app.MapPost("/guests/add", async (
    HttpContext httpContext,
    GuestEntryService guestEntryService) =>
{
    var form = await httpContext.Request.ReadFormAsync();
    var name = form["name"].ToString();
    var notes = form["notes"].ToString();

    try
    {
        if (await guestEntryService.NameAndNotesExistAsync(name, notes))
        {
            return Results.Redirect("/guests?error=duplicate");
        }

        var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
        var username = httpContext.User.Identity?.Name ?? "unknown";

        await guestEntryService.AddAsync(name, notes, userId, username);
    }
    catch (InvalidOperationException exception)
    {
        var error = Uri.EscapeDataString(exception.Message);
        return Results.Redirect($"/guests?error={error}");
    }
    catch
    {
        return Results.Redirect("/guests?error=add-failed");
    }

    return Results.Redirect("/guests");
}).RequireAuthorization("StaffOnly");

app.MapPost("/guests/delete", async (
    HttpContext httpContext,
    GuestEntryService guestEntryService) =>
{
    var form = await httpContext.Request.ReadFormAsync();
    var id = form["id"].ToString();

    if (string.IsNullOrWhiteSpace(id))
    {
        return Results.Redirect("/guests?error=Guest entry is missing an id.");
    }

    try
    {
        await guestEntryService.DeleteAsync(id);
    }
    catch
    {
        return Results.Redirect("/guests?error=Guest could not be deleted.");
    }

    return Results.Redirect("/guests");
}).RequireAuthorization("StaffOnly");

app.MapPost("/guests/update", async (
    HttpContext httpContext,
    GuestEntryService guestEntryService) =>
{
    var form = await httpContext.Request.ReadFormAsync();
    var id = form["id"].ToString();
    var name = form["name"].ToString();
    var notes = form["notes"].ToString();

    if (string.IsNullOrWhiteSpace(id))
    {
        return Results.Redirect("/guests?error=No guest entry is selected for editing.");
    }

    try
    {
        if (await guestEntryService.NameAndNotesExistAsync(name, notes, id))
        {
            return Results.Redirect($"/guests?edit={Uri.EscapeDataString(id)}&error=duplicate");
        }

        await guestEntryService.UpdateAsync(id, name, notes);
    }
    catch (InvalidOperationException exception)
    {
        var error = Uri.EscapeDataString(exception.Message);
        return Results.Redirect($"/guests?edit={Uri.EscapeDataString(id)}&error={error}");
    }
    catch
    {
        return Results.Redirect($"/guests?edit={Uri.EscapeDataString(id)}&error=Guest could not be updated.");
    }

    return Results.Redirect("/guests");
}).RequireAuthorization("StaffOnly");

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

    try
    {
        await accessCodeService.SetGuestAccessCodeAsync(accessCode);
    }
    catch (InvalidOperationException exception)
    {
        var error = Uri.EscapeDataString(exception.Message);
        return Results.Redirect($"/admin?accessCodeMessage={error}");
    }
    catch
    {
        return Results.Redirect("/admin?accessCodeMessage=Guest access code could not be updated.");
    }

    return Results.Redirect("/admin?accessCodeMessage=Guest access code has been updated.");
}).RequireAuthorization("AdminOnly");

app.MapPost("/admin/maintainers/create", async (
    HttpContext httpContext,
    UserService userService) =>
{
    var form = await httpContext.Request.ReadFormAsync();
    var username = form["username"].ToString();
    var displayName = form["displayName"].ToString();
    var password = form["password"].ToString();

    if (string.IsNullOrWhiteSpace(displayName))
    {
        displayName = username;
    }

    try
    {
        await userService.CreateMaintainerAsync(displayName, username, password);
    }
    catch (InvalidOperationException exception)
    {
        var error = Uri.EscapeDataString(exception.Message);
        return Results.Redirect($"/admin?maintainerMessage={error}");
    }
    catch
    {
        return Results.Redirect("/admin?maintainerMessage=Maintainer could not be created.");
    }

    return Results.Redirect("/admin?maintainerMessage=Maintainer has been created.");
}).RequireAuthorization("AdminOnly");

app.MapPost("/admin/maintainers/delete", async (
    HttpContext httpContext,
    UserService userService) =>
{
    var form = await httpContext.Request.ReadFormAsync();
    var id = form["id"].ToString();

    if (string.IsNullOrWhiteSpace(id))
    {
        return Results.Redirect("/admin?maintainerMessage=Maintainer is missing an id.");
    }

    try
    {
        await userService.DeleteMaintainerAsync(id);
    }
    catch
    {
        return Results.Redirect("/admin?maintainerMessage=Maintainer could not be deleted.");
    }

    return Results.Redirect("/admin?maintainerMessage=Maintainer has been deleted.");
}).RequireAuthorization("AdminOnly");

app.MapPost("/admin/reset-setup", async (
    HttpContext httpContext,
    DatabaseBackupService databaseBackupService,
    UserService userService,
    GuestEntryService guestEntryService,
    AccessCodeService accessCodeService,
    SetupStatusService setupStatusService) =>
{
    DatabaseBackupResult backupResult;

    try
    {
        backupResult = await databaseBackupService.BackupAndDropActiveDatabaseAsync("Setup reset from admin panel");
        await setupStatusService.ResetSetupAsync();
        await userService.EnsureIndexesAsync();
        await guestEntryService.EnsureIndexesAsync();
        await accessCodeService.EnsureIndexesAsync();
        await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        httpContext.Session.Clear();
    }
    catch (InvalidOperationException exception)
    {
        var error = Uri.EscapeDataString(exception.Message);
        return Results.Redirect($"/admin?message={error}");
    }
    catch (Exception exception)
    {
        var error = Uri.EscapeDataString($"Setup could not be reset: {exception.Message}");
        return Results.Redirect($"/admin?message={error}");
    }

    var backupDatabase = Uri.EscapeDataString(backupResult.BackupDatabaseName);

    return Results.Redirect(
        $"/setup?error=setup-reset&backupDatabase={backupDatabase}&backedUpCollections={backupResult.CollectionCount}&backedUpDocuments={backupResult.DocumentCount}");
}).RequireAuthorization("AdminOnly");

app.MapPost("/admin/setup-recovery", async (
    AccessCodeService accessCodeService,
    SetupStatusService setupStatusService) =>
{
    try
    {
        if (!await accessCodeService.HasGuestAccessCodeAsync())
        {
            return Results.Redirect("/admin?recoveryMessage=Set a guest access code before completing recovery.");
        }

        await setupStatusService.MarkSetupCompleteAsync();
    }
    catch
    {
        return Results.Redirect("/admin?recoveryMessage=Setup recovery could not be completed.");
    }

    return Results.Redirect("/admin?recoveryMessage=Setup recovery has been completed.");
}).RequireAuthorization("AdminOnly");

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("/setup/complete") ||
        path.StartsWithSegments("/setup/restore") ||
        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("//");
}
