using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
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.ScadaBridge.Commons.Interfaces.Services;

namespace ZB.MOM.WW.ScadaBridge.Security;

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// The configuration section bound to the shared LDAP <c>LdapOptions</c>. Nested
    /// under the existing <c>ScadaBridge:Security</c> section as a <c>Ldap</c> sub-section
    /// so the non-LDAP <see cref="SecurityOptions"/> fields stay
    /// where they are while the LDAP connection settings bind to the shared library.
    /// The Host composition root references this constant to register the shared LDAP
    /// auth (<c>AddZbLdapAuth</c>) before calling <see cref="AddSecurity"/>.
    /// </summary>
    public const string LdapSectionPath = "ScadaBridge:Security:Ldap";

    /// <summary>
    /// Registers the JWT token service, role mapper, cookie authentication, and
    /// authorization policies. This is a component library and therefore takes no
    /// <c>IConfiguration</c> (Options pattern only).
    /// </summary>
    /// <remarks>
    /// LDAP authentication (shared <c>ZB.MOM.WW.Auth.Ldap</c> via <c>AddZbLdapAuth</c>) is
    /// registered by the Host composition root — which calls <c>AddZbLdapAuth</c> with the
    /// nested <see cref="LdapSectionPath"/> <em>before</em> this method — because that
    /// registration is config-coupled (it binds <c>LdapOptions</c> from
    /// <c>IConfiguration</c>) and component libraries must not accept <c>IConfiguration</c>.
    /// </remarks>
    /// <param name="services">The service collection to register into.</param>
    /// <param name="disableLogin">
    /// Dev/test flag (bound from <see cref="Auth.AuthDisableLoginOptions"/> by the Host
    /// composition root). When true the cookie handler is replaced — under the same cookie
    /// scheme name — by an always-succeeding <see cref="Auth.AutoLoginAuthenticationHandler"/>
    /// that authenticates every request as the configured dev user with ALL roles, and a loud
    /// startup warning is emitted. Default false. Never enable in production.
    /// </param>
    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
    public static IServiceCollection AddSecurity(this IServiceCollection services, bool disableLogin = false)
    {
        // ScadaBridge's bespoke LdapAuthService was replaced by the
        // shared ZB.MOM.WW.Auth.Ldap implementation (ScadaBridge was the donor for its
        // hardened bind-then-search / escaping / fail-closed semantics, so this was a
        // behaviour-equivalent re-point). That registration — AddZbLdapAuth, which binds
        // LdapOptions from the nested Ldap sub-section, registers IValidateOptions<LdapOptions>
        // with ValidateOnStart (so a misconfigured directory fails fast at boot —
        // superseding the old SecurityOptionsValidator LDAP checks), and registers
        // ILdapAuthService as a stateless singleton — now lives at the Host composition
        // root (which calls AddZbLdapAuth(configuration, LdapSectionPath) immediately
        // before AddSecurity()), because it is config-coupled and this is a component
        // library.

        services.AddScoped<JwtTokenService>();
        services.AddScoped<RoleMapper>();

        // The cookie OnValidatePrincipal core. Scoped to match the
        // IGroupRoleMapper<string> it depends on (which depends on the Scoped
        // ISecurityRepository). The clock is injected (TimeProvider) so the idle/refresh
        // thresholds can be exercised deterministically in tests; the production default
        // is the wall clock. TryAddSingleton keeps the Host free to register its own.
        services.TryAddSingleton(TimeProvider.System);
        services.AddScoped<CookieSessionValidator>();

        // Audit Actor wiring (Phase 3): the user-facing inbound API audit path
        // sources AuditEvent.Actor from the authenticated principal via this
        // seam. HttpAuditActorAccessor reads IHttpContextAccessor.HttpContext?.User
        // (canonical username claim, Identity.Name fallback) and returns null when
        // there is no authenticated interactive user — so the caller keeps its
        // existing actor/fallback (API-key name, "system"). Registered as a
        // singleton (it is stateless and only dereferences the ambient request);
        // AddHttpContextAccessor is idempotent (TryAdd-based) so calling it here
        // is safe even though the Host's AddCentralUI also registers it.
        services.AddHttpContextAccessor();
        services.AddSingleton<IAuditActorAccessor, HttpAuditActorAccessor>();

        // Auth-adoption: register the shared IGroupRoleMapper<string>
        // seam additively, wrapping RoleMapper to reuse its DB-backed mapping +
        // site-scope union semantics. Scoped to match RoleMapper's lifetime (it
        // depends on the Scoped ISecurityRepository). The existing RoleMapper
        // registration and its call sites are left untouched — login is rewired
        // to consume this seam in a later task.
        services.AddScoped<IGroupRoleMapper<string>, ScadaBridgeGroupRoleMapper>();

        // Fail-fast config guard — RoleRefreshThresholdMinutes must be strictly
        // less than IdleTimeoutMinutes. If they are equal or inverted, a single un-refreshed
        // cycle can exhaust the entire idle window and idle enforcement is silently defeated.
        // SecurityOptionsValidator is registered with ValidateOnStart so a misconfigured
        // appsettings section fails at boot with a clear message rather than behaving subtly
        // incorrectly at runtime. Config-binding stays with the Host (component library must
        // not take IConfiguration), so we only register the validator + ValidateOnStart here.
        services.AddOptions<SecurityOptions>().ValidateOnStart();
        services.AddSingleton<IValidateOptions<SecurityOptions>, SecurityOptionsValidator>();

        // Fixed-window LDAP-bind failure lockout (arch-review P1). A singleton so its
        // per-{username|ip} failure counters are shared across every request on this node;
        // consulted before, and updated after, every LDAP-bind surface (login/token endpoints
        // and the ManagementAuthenticator-fronted management/audit/debug paths).
        services.AddSingleton<LoginThrottle>();

        // Note: the old SecurityOptionsValidator (which fail-fast-validated LdapServer +
        // LdapSearchBase) is gone — those keys moved into the shared LdapOptions, whose
        // LdapOptionsValidator (registered with ValidateOnStart by AddZbLdapAuth above)
        // now enforces Server + SearchBase + ServiceAccountDn + transport at startup. The
        // JWT signing key continues to fail-fast at JwtTokenService construction.

        // Register ASP.NET Core authentication with cookie scheme. Only the static
        // SecurityOptions-independent settings (login/logout paths) are set here; the
        // cookie NAME and the hardened defaults that depend on SecurityOptions (idle
        // timeout, HTTPS policy) are applied via the SecurityOptions-bound PostConfigure
        // below (cookie name through SecurityOptions.CookieName, the rest through
        // ZbCookieDefaults.Apply).
        var authBuilder = services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
        if (disableLogin)
        {
            // DEV/TEST ONLY: replace the cookie handler with an always-succeeding handler registered
            // UNDER the cookie scheme name, so every authorization policy (which names this scheme)
            // authenticates through it with all roles — zero policy changes. No cookie is written;
            // OnValidatePrincipal (idle/refresh) does not apply in this mode. See AuthDisableLoginOptions.
            authBuilder.AddScheme<AuthenticationSchemeOptions, Auth.AutoLoginAuthenticationHandler>(
                CookieAuthenticationDefaults.AuthenticationScheme, _ => { });

            services.AddOptions<Auth.AuthDisableLoginOptions>()
                .PostConfigure<ILoggerFactory>((opts, lf) =>
                    lf.CreateLogger("ZB.MOM.WW.ScadaBridge.Security").LogWarning(
                        "AUTH DISABLED (ScadaBridge:Security:Auth:DisableLogin=true) — every request is " +
                        "authenticated as '{User}' with FULL permissions ({Roles}) across ALL sites. This " +
                        "is a SCADA control surface; dev/test ONLY — never enable in production.",
                        opts.User, string.Join(", ", Roles.All)));
        }
        else
        {
            authBuilder.AddCookie(options =>
            {
                options.LoginPath = "/login";
                options.LogoutPath = "/auth/logout";
                // Cookie.Name is app-owned (not set by ZbCookieDefaults.Apply, so co-hosted
                // ZB apps do not clobber each other) AND now config-driven — it is set from
                // SecurityOptions.CookieName in the PostConfigure below so two ScadaBridge
                // environments sharing a hostname can be given distinct names. HttpOnly /
                // SameSite / SecurePolicy / SlidingExpiration / ExpireTimeSpan are likewise
                // applied there via ZbCookieDefaults.Apply.

                // OnValidatePrincipal enforces the idle timeout and refreshes
                // the role/scope claims from the session's STORED LDAP groups (DB-backed
                // RoleMapper, NO LDAP) so central role-mapping changes take effect
                // mid-session. The lambda is a THIN adapter: it resolves the request-scoped
                // CookieSessionValidator (which holds all the testable idle/refresh logic)
                // and translates its decision into the cookie context calls. It NEVER
                // throws — CookieSessionValidator.ValidateAsync swallows refresh faults and
                // keeps the session (mirrors "LDAP failure: active sessions continue").
                options.Events.OnValidatePrincipal = OnValidatePrincipalAsync;
            });
        }

        // Configure the cookie session as a sliding window so the
        // code matches the documented policy ("sliding refresh, 30-minute idle
        // timeout"). ASP.NET cookie auth exposes a single ExpireTimeSpan plus a
        // SlidingExpiration flag — it cannot natively model a 15-minute sliding
        // token AND a separate 30-minute absolute idle cap. The faithful
        // interpretation: the cookie window IS the idle timeout
        // (SecurityOptions.IdleTimeoutMinutes, default 30) and SlidingExpiration
        // renews it on activity (the middleware re-issues the cookie once past
        // the halfway mark of the window). An active user is therefore kept
        // signed in; an idle user is signed out after the idle timeout. The
        // 15-minute JwtExpiryMinutes governs the lifetime of the embedded JWT
        // itself (see JwtTokenService) — a separate layer from the cookie
        // session window. Bound here via PostConfigure so SecurityOptions
        // (configured by the Host after AddSecurity) is honoured.
        //
        // The cookie hardening (HttpOnly=true, SameSite=Strict,
        // SecurePolicy, SlidingExpiration=true, ExpireTimeSpan=idle) now comes from
        // the shared ZbCookieDefaults.Apply, with requireHttps + idleTimeout driven
        // by SecurityOptions so behaviour (30-min sliding idle window, HTTPS-only
        // unless explicitly opted out) is preserved.
        // Harmless/unused when disableLogin is true: the cookie handler is not registered,
        // so this CookieAuthenticationOptions PostConfigure has no scheme to configure.
        services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IOptions<SecurityOptions>, ILoggerFactory>((cookieOptions, securityOptions, loggerFactory) =>
            {
                ZbCookieDefaults.Apply(
                    cookieOptions,
                    requireHttps: securityOptions.Value.RequireHttpsCookie,
                    idleTimeout: TimeSpan.FromMinutes(securityOptions.Value.IdleTimeoutMinutes));

                // App-owned, config-driven cookie name (ScadaBridge:Security:CookieName).
                // ZbCookieDefaults.Apply intentionally leaves the name untouched. A
                // blank/whitespace value falls back to the canonical default so a
                // misconfiguration cannot produce an unnamed cookie.
                var cookieName = securityOptions.Value.CookieName;
                cookieOptions.Cookie.Name = string.IsNullOrWhiteSpace(cookieName)
                    ? SecurityOptions.DefaultCookieName
                    : cookieName;

                // When the operator opts out of HTTPS-only cookies,
                // log a Warning so an HTTP-only deployment is at least audible in
                // the startup log. The cookie carries the embedded JWT bearer
                // credential — over plain HTTP that travels in cleartext on every
                // request. The default is true; this branch fires only on an
                // explicit opt-out (typically the dev Docker cluster). Apply sets
                // SecurePolicy=SameAsRequest in that case.
                if (!securityOptions.Value.RequireHttpsCookie)
                {
                    loggerFactory.CreateLogger("ZB.MOM.WW.ScadaBridge.Security").LogWarning(
                        "SecurityOptions:RequireHttpsCookie is DISABLED — auth cookie SecurePolicy is SameAsRequest. The cookie-embedded JWT will be transmitted in cleartext over plain HTTP. This setting is intended for local dev only — set SecurityOptions:RequireHttpsCookie=true in production.");
                }
            });

        services.AddScadaBridgeAuthorization();

        return services;
    }

    /// <summary>
    /// The thin <see cref="CookieAuthenticationEvents.OnValidatePrincipal"/>
    /// adapter. It resolves the request-scoped <see cref="CookieSessionValidator"/>,
    /// asks it for a decision, and applies it to the cookie context:
    /// <list type="bullet">
    ///   <item><see cref="SessionValidationAction.Reject"/> → <see cref="CookieValidatePrincipalContext.RejectPrincipal"/> + sign out (idle-timeout — the only sign-out path).</item>
    ///   <item><see cref="SessionValidationAction.Replace"/> → <see cref="CookieValidatePrincipalContext.ReplacePrincipal"/> + <c>ShouldRenew = true</c> (role mapping refreshed).</item>
    ///   <item><see cref="SessionValidationAction.Keep"/> → no-op (no refresh due, or a swallowed refresh fault).</item>
    /// </list>
    /// All logic lives in <see cref="CookieSessionValidator.ValidateAsync"/>, which never
    /// throws, so this adapter cannot bubble an exception out into the request pipeline.
    /// </summary>
    /// <param name="context">The cookie validation context supplied by the middleware.</param>
    /// <returns>A task that completes when the decision has been applied.</returns>
    internal static async Task OnValidatePrincipalAsync(CookieValidatePrincipalContext context)
    {
        var validator = context.HttpContext.RequestServices.GetRequiredService<CookieSessionValidator>();

        var result = await validator
            .ValidateAsync(context.Principal, context.HttpContext.RequestAborted)
            .ConfigureAwait(false);

        await ApplyValidationResultAsync(context, result).ConfigureAwait(false);
    }

    /// <summary>
    /// Applies a <see cref="SessionValidationResult"/> to a
    /// <see cref="CookieValidatePrincipalContext"/>: the pure decision-application
    /// step extracted from <see cref="OnValidatePrincipalAsync"/> so it can be
    /// exercised in unit tests without a live DI container resolving
    /// <see cref="CookieSessionValidator"/>.
    /// </summary>
    /// <param name="context">The cookie validation context to mutate.</param>
    /// <param name="result">The decision produced by <see cref="CookieSessionValidator.ValidateAsync"/>.</param>
    /// <returns>A task that completes when the result has been applied.</returns>
    internal static async Task ApplyValidationResultAsync(
        CookieValidatePrincipalContext context,
        SessionValidationResult result)
    {
        switch (result.Action)
        {
            case SessionValidationAction.Reject:
                // Idle-timeout: drop the principal AND clear the cookie so the next
                // request is treated as anonymous and redirected to /login.
                context.RejectPrincipal();
                await context.HttpContext
                    .SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme)
                    .ConfigureAwait(false);
                break;

            case SessionValidationAction.Replace when result.Principal is not null:
                // Role mapping refreshed from stored groups — swap in the rebuilt
                // principal and re-issue the cookie so the new claims persist.
                context.ReplacePrincipal(result.Principal);
                context.ShouldRenew = true;
                break;

            case SessionValidationAction.Keep:
            default:
                // Leave the principal untouched.
                break;
        }
    }

    /// <summary>
    /// Registers security-related Akka actors (placeholder for future actor registrations).
    /// </summary>
    /// <param name="services">The service collection to register into.</param>
    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
    public static IServiceCollection AddSecurityActors(this IServiceCollection services)
    {
        // Phase 0: placeholder for Akka actor registration
        return services;
    }
}
