using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Security.Audit;
using ZB.MOM.WW.OtOpcUa.Security.Auth;
using ZB.MOM.WW.OtOpcUa.Security.Jwt;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;

namespace ZB.MOM.WW.OtOpcUa.Security;

/// <summary>
///     DI registration for OtOpcUa auth. Single Cookie scheme (the JWT lives inside the
///     cookie as its credential payload); no JwtBearer parallel scheme. Matches ScadaBridge
///     structurally — see <c>docs/plans/2026-05-29-auth-alignment-design.md</c>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    ///     Wires cookie authentication, DataProtection key persistence to ConfigDb,
    ///     LDAP services, and the LDAP-backed JwtTokenService. Browser flows redirect to
    ///     <c>/login</c>; AJAX/JSON callers receive 401 (handled by the framework's default
    ///     challenge heuristic).
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <param name="configuration">The application configuration root.</param>
    /// <returns>The same service collection, for chaining.</returns>
    public static IServiceCollection AddOtOpcUaAuth(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddOptions<JwtOptions>().Bind(configuration.GetSection(JwtOptions.SectionName));
        services.AddOptions<OtOpcUaCookieOptions>().Bind(configuration.GetSection(OtOpcUaCookieOptions.SectionName));
        services.AddOptions<LdapOptions>().Bind(configuration.GetSection(LdapOptions.SectionName));
        services.AddOptions<AuthDisableLoginOptions>().Bind(configuration.GetSection(AuthDisableLoginOptions.SectionName));

        services.AddSingleton<JwtTokenService>();

        // IHttpContextAccessor is not registered by default — call AddHttpContextAccessor()
        // so HttpAuditActorAccessor and any Blazor/minimal-API component that reads the current
        // HTTP context by injection can resolve it. AddHttpContextAccessor is idempotent (internal
        // TryAdd), so calling it here is safe even if the host also calls it elsewhere.
        services.AddHttpContextAccessor();

        // IAuditActorAccessor — resolves the authenticated HTTP principal's actor string for use
        // as the Actor field when constructing a canonical ZB.MOM.WW.Audit.AuditEvent. Registered
        // Scoped so it correctly follows the request scope used by Blazor Server and minimal-API
        // endpoints.
        services.TryAddScoped<IAuditActorAccessor, HttpAuditActorAccessor>();

        // Singleton — OtOpcUaLdapAuthService is stateless (the shared-library directory client it
        // wraps opens/disposes an LdapConnection per call) and must be consumable by the Singleton
        // LdapOpcUaUserAuthenticator on driver-role nodes. This is the app's ILdapAuthService: it
        // adds the Enabled master switch + DevStubMode bypass on top of the shared ZB.MOM.WW.Auth.Ldap
        // service. TryAdd so a fused admin+driver node (which also registers it in Program.cs for the
        // driver path) ends up with exactly one descriptor regardless of registration order.
        services.TryAddSingleton<ILdapAuthService, OtOpcUaLdapAuthService>();

        // Shared ZB.MOM.WW.Auth group→role mapper seam. Wraps the existing RoleMapper.Map +
        // RoleMapper.Merge logic; consumed by AuthEndpoints.cs (/auth/login) and by
        // LdapOpcUaUserAuthenticator (OPC UA UserName session auth). Scoped to match
        // ILdapGroupRoleMappingService (DbContext-backed, registered Scoped) — a singleton here
        // would capture the scoped DB service.
        services.TryAddScoped<IGroupRoleMapper<string>, OtOpcUaGroupRoleMapper>();

        services.AddDataProtection()
            .PersistKeysToDbContext<OtOpcUaConfigDbContext>()
            .SetApplicationName("OtOpcUa");

        var disableLogin = configuration
            .GetSection(AuthDisableLoginOptions.SectionName)
            .GetValue<bool>(nameof(AuthDisableLoginOptions.DisableLogin));

        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 FallbackPolicy + FleetAdmin + DriverOperator (which all
            // name this scheme) authenticate through it and pass with all roles — zero policy changes.
            authBuilder.AddScheme<AuthenticationSchemeOptions, AutoLoginAuthenticationHandler>(
                CookieAuthenticationDefaults.AuthenticationScheme, _ => { });

            // Loud, once-at-first-resolve warning (mirrors the cookie RequireHttps warning idiom below).
            services.AddOptions<AuthDisableLoginOptions>()
                .PostConfigure<ILoggerFactory>((opts, lf) =>
                {
                    lf.CreateLogger("ZB.MOM.WW.OtOpcUa.Security").LogWarning(
                        "AdminUI LOGIN DISABLED (Security:Auth:DisableLogin=true) — every request is " +
                        "authenticated as '{User}' with FULL permissions ({Roles}). Dev/test only; never " +
                        "enable in production.", opts.User, string.Join(",", DevAuthRoles.All));
                });
        }
        else
        {
            authBuilder.AddCookie(o =>
            {
                // Static fields only — Name / ExpireTimeSpan / SecurePolicy / SlidingExpiration /
                // HttpOnly / SameSite are applied from OtOpcUaCookieOptions via ZbCookieDefaults
                // in the PostConfigure block below.
                o.LoginPath = "/login";
                o.LogoutPath = "/auth/logout";
                // No OnRedirectToLogin / OnRedirectToAccessDenied overrides — let the framework's
                // built-in IsAjaxRequest heuristic do its thing (302 for browsers, 401 for AJAX).
            });
        }

        // Externalised cookie config — mirrors ScadaBridge's PostConfigure pattern. Fixes a
        // pre-existing latent bug where OtOpcUaCookieOptions was bound but ignored.
        // ZbCookieDefaults.Apply sets HttpOnly=true, SameSite=Strict, SlidingExpiration=true,
        // SecurePolicy, and ExpireTimeSpan; we then set the app-specific cookie name on top.
        services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IOptions<OtOpcUaCookieOptions>, ILoggerFactory>((cookieOpts, ourOpts, lf) =>
            {
                var v = ourOpts.Value;

                // Apply canonical hardened defaults (HttpOnly, SameSite=Strict, SlidingExpiration,
                // SecurePolicy, ExpireTimeSpan). Cookie name is NOT touched by ZbCookieDefaults —
                // we set it below so each app keeps its own distinct cookie name.
                ZbCookieDefaults.Apply(
                    cookieOpts,
                    requireHttps: v.RequireHttpsCookie,
                    idleTimeout: TimeSpan.FromMinutes(v.ExpiryMinutes));

                // Keep OtOpcUa's own cookie name (default "ZB.MOM.WW.OtOpcUa.Auth").
                cookieOpts.Cookie.Name = v.Name;

                if (!v.RequireHttpsCookie)
                {
                    lf.CreateLogger("ZB.MOM.WW.OtOpcUa.Security").LogWarning(
                        "Security:Cookie:RequireHttpsCookie is DISABLED — auth cookie SecurePolicy is " +
                        "SameAsRequest. The cookie-embedded JWT will travel in cleartext over plain HTTP. " +
                        "Intended for local dev only — set Security:Cookie:RequireHttpsCookie=true in production.");
                }
            });

        services.AddAuthorization(o =>
        {
            o.FallbackPolicy = new AuthorizationPolicyBuilder(
                    CookieAuthenticationDefaults.AuthenticationScheme)
                .RequireAuthenticatedUser()
                .Build();

            // DriverOperator (policy NAME kept stable): may issue Reconnect/Restart commands against
            // live driver instances from the Admin UI DriverStatusPanel. The role STRINGS it requires
            // are the canonical control-plane roles: Operator (was DriverOperator) and
            // Administrator (was FleetAdmin). Map LDAP group → role via GroupToRole in appsettings
            // (e.g. "ot-driver-operator": "Operator").
            o.AddPolicy(AdminUiPolicies.DriverOperator, policy =>
                policy.RequireRole(DevAuthRoles.Operator, "Administrator"));

            // FleetAdmin (policy NAME kept stable): full administrative access; gates fleet-wide pages
            // such as RoleGrants. Requires the canonical Administrator role (was FleetAdmin).
            o.AddPolicy(AdminUiPolicies.FleetAdmin, policy => policy.RequireRole("Administrator"));

            // ConfigEditor: may author configuration (UNS, equipment, clusters, drivers incl. live
            // ResilienceConfig, ACLs, namespaces, scripts) and trigger deploys. Same semantics the
            // Deployments/Scripts/ScriptEdit pages previously spelled as Roles="Administrator,Designer".
            o.AddPolicy(AdminUiPolicies.ConfigEditor, policy =>
                policy.RequireRole("Administrator", "Designer"));

            // AuthenticatedRead: explicit name for "any authenticated user" so read-only pages can carry
            // a non-bare attribute and the page-authorization guard can ban bare [Authorize] outright.
            // Semantically identical to the FallbackPolicy.
            o.AddPolicy(AdminUiPolicies.AuthenticatedRead, policy =>
                policy.RequireAuthenticatedUser());
        });

        return services;
    }
}
