using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.HttpResults;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard.Components;
using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard;

/// <summary>Endpoint extensions for registering the gateway dashboard routes.</summary>
public static class DashboardEndpointRouteBuilderExtensions
{
    /// <summary>Maps all gateway dashboard routes including login, logout, and Razor components.</summary>
    /// <param name="endpoints">The endpoint route builder.</param>
    /// <returns>The route builder for chaining.</returns>
    public static IEndpointRouteBuilder MapGatewayDashboard(this IEndpointRouteBuilder endpoints)
    {
        IConfiguration configuration = endpoints.ServiceProvider.GetRequiredService<IConfiguration>();
        IConfigurationSection dashboardSection = configuration
            .GetSection($"{GatewayOptions.SectionName}:Dashboard");

        if (bool.TryParse(dashboardSection["Enabled"], out bool enabled) && !enabled)
        {
            return endpoints;
        }

        endpoints.MapGet(
                "/login",
                (HttpContext httpContext, IAntiforgery antiforgery) => GetLoginAsync(httpContext, antiforgery))
            .AllowAnonymous()
            .WithName("DashboardLogin");

        endpoints.MapPost(
                "/login",
                (HttpContext httpContext, IAntiforgery antiforgery, IDashboardAuthenticator authenticator) =>
                    PostLoginAsync(httpContext, antiforgery, authenticator))
            .AllowAnonymous()
            .WithName("DashboardLoginPost");

        endpoints.MapPost(
                "/logout",
                (HttpContext httpContext, IAntiforgery antiforgery) => PostLogoutAsync(httpContext, antiforgery))
            .AllowAnonymous()
            .WithName("DashboardLogout");

        endpoints.MapGet("/denied", () => Results.Content(
                RenderPage("Access denied", "<p>The signed-in user is not authorized for dashboard access.</p>"),
                "text/html"))
            .AllowAnonymous()
            .WithName("DashboardAccessDenied");

        // SignalR hubs. Authorization is enforced on the hub class via
        // [Authorize(Policy = HubClientsPolicy)] so the policy accepts either
        // the dashboard cookie or a HubToken bearer.
        endpoints.MapHub<DashboardSnapshotHub>("/hubs/snapshot");
        endpoints.MapHub<AlarmsHub>("/hubs/alarms");
        endpoints.MapHub<EventsHub>("/hubs/events");

        // Bearer-token mint endpoint. The cookie-authenticated browser hits
        // this from JS before opening a hub connection; the hub then accepts
        // the returned token via the HubToken scheme. Restricting access to
        // the cookie scheme keeps the bearer issuance path from being
        // self-bootstrapped from a previous bearer.
        endpoints.MapGet(
                "/hubs/token",
                (HttpContext httpContext, HubTokenService tokens) =>
                {
                    string token = tokens.Issue(httpContext.User);
                    return Results.Json(new { token });
                })
            .RequireAuthorization(DashboardAuthenticationDefaults.ViewerPolicy)
            .WithName("DashboardHubToken");

        // Every dashboard Razor component requires a viewer-or-admin role. The
        // login/logout/denied endpoints above opt out via AllowAnonymous(); an
        // unauthenticated request to a component route is challenged by the
        // cookie scheme and redirected to /login.
        endpoints.MapRazorComponents<App>()
            .AddInteractiveServerRenderMode()
            .RequireAuthorization(DashboardAuthenticationDefaults.ViewerPolicy);

        return endpoints;
    }

    private static Task<ContentHttpResult> GetLoginAsync(
        HttpContext httpContext,
        IAntiforgery antiforgery)
    {
        string returnUrl = SanitizeReturnUrl(httpContext.Request.Query["returnUrl"].ToString());

        return Task.FromResult(TypedResults.Content(
            RenderLoginPage(httpContext, antiforgery, returnUrl, failureMessage: null),
            "text/html"));
    }

    private static async Task<IResult> PostLoginAsync(
        HttpContext httpContext,
        IAntiforgery antiforgery,
        IDashboardAuthenticator authenticator)
    {
        await antiforgery.ValidateRequestAsync(httpContext).ConfigureAwait(false);

        IFormCollection form = await httpContext.Request
            .ReadFormAsync(httpContext.RequestAborted)
            .ConfigureAwait(false);
        string returnUrl = SanitizeReturnUrl(form["returnUrl"].ToString());

        DashboardAuthenticationResult result = await authenticator
            .AuthenticateAsync(
                form["username"].ToString(),
                form["password"].ToString(),
                httpContext.RequestAborted)
            .ConfigureAwait(false);

        if (!result.Succeeded || result.Principal is null)
        {
            return TypedResults.Content(
                RenderLoginPage(httpContext, antiforgery, returnUrl, result.FailureMessage),
                "text/html",
                statusCode: StatusCodes.Status401Unauthorized);
        }

        await httpContext
            .SignInAsync(DashboardAuthenticationDefaults.AuthenticationScheme, result.Principal)
            .ConfigureAwait(false);

        return Results.LocalRedirect(returnUrl);
    }

    private static async Task<IResult> PostLogoutAsync(
        HttpContext httpContext,
        IAntiforgery antiforgery)
    {
        await antiforgery.ValidateRequestAsync(httpContext).ConfigureAwait(false);
        await httpContext
            .SignOutAsync(DashboardAuthenticationDefaults.AuthenticationScheme)
            .ConfigureAwait(false);

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

    private static string RenderLoginPage(
        HttpContext httpContext,
        IAntiforgery antiforgery,
        string returnUrl,
        string? failureMessage)
    {
        AntiforgeryTokenSet tokens = antiforgery.GetAndStoreTokens(httpContext);
        string requestToken = tokens.RequestToken ?? string.Empty;
        string alert = string.IsNullOrWhiteSpace(failureMessage)
            ? string.Empty
            : $"<p class=\"alert alert-danger\" role=\"alert\">{HtmlEncoder.Default.Encode(failureMessage)}</p>";

        string body = $"""
            <section class="dashboard-login">
                {alert}
                <form method="post" action="/login" class="card login-card">
                    <div class="card-body">
                        <input name="{tokens.FormFieldName}" type="hidden" value="{HtmlEncoder.Default.Encode(requestToken)}" />
                        <input name="returnUrl" type="hidden" value="{HtmlEncoder.Default.Encode(returnUrl)}" />
                        <div class="mb-3">
                            <label for="username" class="form-label">Username</label>
                            <input id="username" name="username" type="text" autocomplete="username" class="form-control" />
                        </div>
                        <div class="mb-3">
                            <label for="password" class="form-label">Password</label>
                            <input id="password" name="password" type="password" autocomplete="current-password" class="form-control" />
                        </div>
                        <button type="submit" class="btn btn-primary">Sign in</button>
                    </div>
                </form>
            </section>
            """;

        return RenderPage("Dashboard Sign In", body);
    }

    private static string RenderPage(string title, string body)
    {
        return $"""
            <!doctype html>
            <html lang="en">
            <head>
                <meta charset="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <title>{HtmlEncoder.Default.Encode(title)}</title>
                <link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css" />
                <link rel="stylesheet" href="/css/theme.css" />
                <link rel="stylesheet" href="/css/dashboard.css" />
            </head>
            <body class="dashboard-body">
                <header class="app-bar">
                    <span class="brand"><span class="mark">&#9646;</span> MXAccess Gateway</span>
                </header>
                <main class="page">
                    <div class="dashboard-page-header">
                        <h1>{HtmlEncoder.Default.Encode(title)}</h1>
                    </div>
                    {body}
                </main>
                <script src="/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
            </body>
            </html>
            """;
    }

    private static string SanitizeReturnUrl(string? returnUrl)
    {
        if (string.IsNullOrWhiteSpace(returnUrl)
            || !returnUrl.StartsWith("/", StringComparison.Ordinal)
            || returnUrl.StartsWith("//", StringComparison.Ordinal)
            || Uri.TryCreate(returnUrl, UriKind.Absolute, out _))
        {
            return "/";
        }

        return returnUrl;
    }
}
