using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Security.Audit;

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

/// <summary>
/// Extension methods for configuring the gateway dashboard services.
/// </summary>
public static class DashboardServiceCollectionExtensions
{
    /// <summary>
    /// Registers all dashboard services, authentication, and Razor components.
    /// </summary>
    /// <param name="services">Service collection to register services.</param>
    /// <param name="configuration">
    /// Application configuration, used to bind the shared LDAP provider's options
    /// from the <c>MxGateway:Ldap</c> section. Also read to select the dashboard
    /// authentication scheme via the <c>MxGateway:Dashboard:DisableLogin</c> dev flag.
    /// </param>
    /// <returns>The service collection, for chaining.</returns>
    public static IServiceCollection AddGatewayDashboard(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Dashboard logins delegate bind/search to the shared ZB.MOM.WW.Auth.Ldap
        // provider. Its LdapOptions bind straight from MxGateway:Ldap (the gateway's
        // LdapOptions field names match the shared options: Transport / AllowInsecure /
        // SearchBase / ServiceAccount* / *Attribute). AddZbLdapAuth also adds a
        // ValidateOnStart() so an insecure-transport misconfiguration fails fast at boot.
        services.AddZbLdapAuth(configuration, "MxGateway:Ldap");

        services.AddSingleton<IDashboardSnapshotService, DashboardSnapshotService>();
        services.AddSingleton<IDashboardSnapshotFeed, DashboardSnapshotFeed>();
        services.AddSingleton<IDashboardLiveDataService, DashboardLiveDataService>();
        services.AddSingleton<IDashboardAuthenticator, DashboardAuthenticator>();
        services.AddSingleton<IGroupRoleMapper<string>, DashboardGroupRoleMapper>();
        services.AddSingleton<DashboardApiKeyAuthorization>();
        services.AddSingleton<IDashboardApiKeyManagementService, DashboardApiKeyManagementService>();
        services.AddSingleton<IDashboardSessionAdminService, DashboardSessionAdminService>();
        // Singleton and stateless: it reads the session registry and options per call, and is
        // consulted from both subscribe seams (the EventsHub join and the session-details page's
        // in-process subscription).
        services.AddSingleton<IDashboardSessionAcl, DashboardSessionAcl>();
        // Singleton, and the only consumer scope left is HubTokenAuthenticationHandler plus
        // the /hubs/token endpoint: server-rendered pages read the in-process feeds, so
        // nothing in this process builds a hub connection or needs a token for one.
        services.AddSingleton<HubTokenService>();
        services.AddScoped<IDashboardBrowseService, DashboardBrowseService>();
        // Singleton: EventsHub instances are transient (one per hub invocation), so the
        // subscriber bookkeeping they share with the broadcaster must outlive them.
        services.AddSingleton<Hubs.EventsHubViewerRegistry>();

        // One instance behind two interfaces, registered concretely and forwarded: the
        // publish side (IDashboardEventBroadcaster, driven by the session pipeline) and
        // the in-process subscribe side (IDashboardSessionEventSubscriber, used by the
        // session-details page) share subscriber bookkeeping, so resolving them to two
        // instances would leave the page subscribed to a mirror nobody publishes to.
        services.AddSingleton<Hubs.DashboardEventBroadcaster>();
        services.AddSingleton<Hubs.IDashboardEventBroadcaster>(
            static provider => provider.GetRequiredService<Hubs.DashboardEventBroadcaster>());
        services.AddSingleton<Hubs.IDashboardSessionEventSubscriber>(
            static provider => provider.GetRequiredService<Hubs.DashboardEventBroadcaster>());
        services.AddSingleton<Hubs.DashboardSnapshotHubConnectionCounter>();
        services.AddHostedService<Hubs.DashboardSnapshotPublisher>();
        services.AddHostedService<Hubs.AlarmsHubPublisher>();
        services.AddHttpContextAccessor();
        services.AddSingleton<IAuditActorAccessor, HttpAuditActorAccessor>();
        services.AddAntiforgery();
        services.AddCascadingAuthenticationState();
        services.AddRazorComponents()
            .AddInteractiveServerComponents();
        services.AddSignalR();

        // DEV/TEST ONLY. Read directly from configuration here because authentication scheme
        // registration runs before options binding. Key mirrors DashboardOptions.DisableLogin.
        bool disableLogin = configuration.GetValue<bool>("MxGateway:Dashboard:DisableLogin");

        AuthenticationBuilder authentication =
            services.AddAuthentication(DashboardAuthenticationDefaults.AuthenticationScheme);

        if (disableLogin)
        {
            // Register an always-authenticating handler UNDER the cookie scheme name, so the
            // Viewer/Admin/HubClients policies (which all resolve this scheme) authenticate
            // through it as the multi-role dev user — zero policy or page changes.
            authentication.AddScheme<AuthenticationSchemeOptions, DashboardAutoLoginAuthenticationHandler>(
                DashboardAuthenticationDefaults.AuthenticationScheme,
                _ => { });

            // Loud warning, emitted on first resolution of GatewayOptions (i.e. on the first
            // request/options access, not guaranteed at process start). Dev-only safety notice.
            services.AddOptions<GatewayOptions>().PostConfigure<ILoggerFactory>((gatewayOptions, loggerFactory) =>
                loggerFactory
                    .CreateLogger("ZB.MOM.WW.MxGateway.Server.Dashboard.DisableLogin")
                    .LogWarning(
                        "DASHBOARD LOGIN DISABLED (MxGateway:Dashboard:DisableLogin=true) — every request is "
                        + "authenticated as '{User}' with full permissions ({Roles}). Dev/test only; never "
                        + "enable in production.",
                        string.IsNullOrWhiteSpace(gatewayOptions.Dashboard.AutoLoginUser)
                            ? DashboardAutoLoginAuthenticationHandler.DefaultUser
                            : gatewayOptions.Dashboard.AutoLoginUser!.Trim(),
                        $"{DashboardRoles.Admin}, {DashboardRoles.Viewer}"));
        }
        else
        {
            authentication.AddCookie(DashboardAuthenticationDefaults.AuthenticationScheme, cookieOptions =>
            {
                // Hardened defaults (HttpOnly, SameSite=Strict, SecurePolicy, SlidingExpiration,
                // ExpireTimeSpan) via the shared ZbCookieDefaults.Apply. requireHttps is set to
                // its default (true / Always) here and overridden per-environment by the
                // PostConfigure below; the 8-hour idle timeout is preserved (not the 30-min default).
                ZbCookieDefaults.Apply(cookieOptions, requireHttps: true, idleTimeout: TimeSpan.FromHours(8));
                // Cookie name, path, and redirect paths are MxGateway-specific — set after Apply
                // so they are never overwritten by the shared helper (Apply intentionally skips name).
                // This is the canonical default; it is overridden per-environment from
                // DashboardOptions.CookieName by the PostConfigure below.
                cookieOptions.Cookie.Name = DashboardAuthenticationDefaults.CookieName;
                cookieOptions.Cookie.Path = "/";
                cookieOptions.LoginPath = "/login";
                cookieOptions.LogoutPath = "/logout";
                cookieOptions.AccessDeniedPath = "/denied";
            });
        }

        authentication.AddScheme<AuthenticationSchemeOptions, HubTokenAuthenticationHandler>(
            DashboardAuthenticationDefaults.HubAuthenticationScheme,
            _ => { });

        // Honour DashboardOptions.RequireHttpsCookie (default true / Always; set false for dev
        // HTTP deployments → SameAsRequest) and the optional per-environment cookie-name
        // override. Both run after the inline AddCookie config above, so they win.
        services.AddOptions<CookieAuthenticationOptions>(DashboardAuthenticationDefaults.AuthenticationScheme)
            .Configure<IOptions<GatewayOptions>>((cookieOptions, gatewayOptions) =>
            {
                cookieOptions.Cookie.SecurePolicy = gatewayOptions.Value.Dashboard.RequireHttpsCookie
                    ? CookieSecurePolicy.Always
                    : CookieSecurePolicy.SameAsRequest;

                // Config-driven cookie name (MxGateway:Dashboard:CookieName). An explicit override
                // always wins. With no override, restore the __Host- prefix when the cookie is
                // guaranteed Secure (RequireHttpsCookie true → SecurePolicy Always): the __Host-
                // browser guarantees (Secure required, no Domain, Path=/) hold only for a Secure
                // cookie, and a __Host- cookie without Secure is silently dropped — so the prefix
                // is never applied unless SecurePolicy is Always. Otherwise keep the plain
                // canonical default set by AddCookie above, so a misconfiguration cannot unname it.
                var cookieName = gatewayOptions.Value.Dashboard.CookieName;
                if (!string.IsNullOrWhiteSpace(cookieName))
                {
                    cookieOptions.Cookie.Name = cookieName;
                }
                else if (cookieOptions.Cookie.SecurePolicy == CookieSecurePolicy.Always)
                {
                    cookieOptions.Cookie.Name = DashboardAuthenticationDefaults.SecureCookieName;
                }
            });

        services.AddAuthorization(authorization =>
        {
            authorization.AddPolicy(
                DashboardAuthenticationDefaults.ViewerPolicy,
                policy => policy.AddRequirements(DashboardAuthorizationRequirement.AnyDashboardRole));
            authorization.AddPolicy(
                DashboardAuthenticationDefaults.AdminPolicy,
                policy => policy.AddRequirements(DashboardAuthorizationRequirement.AdminOnly));
            authorization.AddPolicy(
                DashboardAuthenticationDefaults.HubClientsPolicy,
                policy => policy
                    .AddAuthenticationSchemes(
                        DashboardAuthenticationDefaults.AuthenticationScheme,
                        DashboardAuthenticationDefaults.HubAuthenticationScheme)
                    .AddRequirements(DashboardAuthorizationRequirement.AnyDashboardRole));
        });
        services.AddSingleton<IAuthorizationHandler, DashboardAuthorizationHandler>();

        return services;
    }
}
