using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Security;
using ZB.MOM.WW.ScadaBridge.Security.Auth;

namespace ZB.MOM.WW.ScadaBridge.ManagementService;

/// <summary>
/// Shared HTTP-Basic → LDAP authentication for the management/CLI surfaces
/// (<see cref="ManagementEndpoints"/>, <see cref="AuditEndpoints"/>, and the debug-stream hub).
/// Centralising the flow here means the dev/test <c>DisableLogin</c> bypass is applied
/// <em>identically</em> on every CLI-facing surface.
/// </summary>
/// <remarks>
/// <para>
/// <c>ScadaBridge:Security:Auth:DisableLogin</c> swaps the <em>cookie</em> authentication
/// scheme for <c>AutoLoginAuthenticationHandler</c>, which lets the interactive browser UI
/// in without credentials. The CLI does not use cookies — it authenticates the
/// <c>POST /management</c> (and audit REST) surfaces with HTTP Basic, which run their own
/// manual Basic → LDAP check. That check is independent of the cookie scheme, so without the
/// bypass below the CLI is locked out of a login-disabled deployment whenever LDAP is not in
/// use (the bind fails and every call returns <c>401 AUTH_FAILED</c>).
/// </para>
/// <para>
/// The bypass mirrors <c>AutoLoginAuthenticationHandler</c> exactly: the configured dev user
/// (default <c>multi-role</c>) with ALL <see cref="Roles.All"/> roles, system-wide (empty
/// <see cref="AuthenticatedUser.PermittedSiteIds"/>). The startup <c>AUTH DISABLED</c> warning
/// (emitted by <c>AddSecurity</c>) already announces this posture. Dev/test ONLY.
/// </para>
/// </remarks>
public static class ManagementAuthenticator
{
    /// <summary>Outcome of <see cref="AuthenticateAsync"/>: exactly one of the two fields is set.</summary>
    /// <param name="User">The authenticated principal on success; otherwise <see langword="null"/>.</param>
    /// <param name="Failure">The 401 <see cref="IResult"/> to return on failure; otherwise <see langword="null"/>.</param>
    public readonly record struct AuthOutcome(AuthenticatedUser? User, IResult? Failure);

    /// <summary>
    /// When <c>ScadaBridge:Security:Auth:DisableLogin</c> is true, returns the synthesized dev
    /// principal (configured user, ALL roles, system-wide) that mirrors the cookie-scheme
    /// <c>AutoLoginAuthenticationHandler</c> — so the Basic-Auth CLI surfaces are reachable in a
    /// login-disabled (e.g. no-LDAP) deployment. Returns <see langword="null"/> when the flag is
    /// off, in which case the caller must fall back to real Basic → LDAP authentication.
    /// Dev/test ONLY.
    /// </summary>
    /// <param name="context">The current HTTP request context (used to resolve the options).</param>
    /// <returns>The dev principal when login is disabled; otherwise <see langword="null"/>.</returns>
    public static AuthenticatedUser? TryDisableLoginUser(HttpContext context)
    {
        var opts = context.RequestServices.GetService<IOptions<AuthDisableLoginOptions>>()?.Value;
        if (opts is null || !opts.DisableLogin)
            return null;

        var user = string.IsNullOrWhiteSpace(opts.User) ? "multi-role" : opts.User;
        return new AuthenticatedUser(user, user, Roles.All, Array.Empty<string>());
    }

    /// <summary>
    /// Authenticates a management/CLI request. Honours the dev/test <c>DisableLogin</c> bypass
    /// first (<see cref="TryDisableLoginUser"/>); otherwise decodes HTTP Basic Auth, binds
    /// against LDAP, and resolves roles. Returns a populated <see cref="AuthenticatedUser"/> on
    /// success, or an <see cref="IResult"/> carrying the 401 response on any failure.
    /// </summary>
    /// <param name="context">The HTTP request to authenticate.</param>
    /// <returns>An <see cref="AuthOutcome"/> carrying either the user or the 401 failure result.</returns>
    public static async Task<AuthOutcome> AuthenticateAsync(HttpContext context)
    {
        // Dev/test bypass: when login is disabled, the CLI surface auto-authenticates as the
        // configured dev user — no credential check — mirroring the interactive cookie handler.
        var bypass = TryDisableLoginUser(context);
        if (bypass is not null)
            return new AuthOutcome(bypass, null);

        // 1. Decode Basic Auth
        var authHeader = context.Request.Headers.Authorization.ToString();
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
        {
            return new AuthOutcome(null, Results.Json(
                new { error = "Authorization header required (Basic scheme).", code = "AUTH_FAILED" }, statusCode: 401));
        }

        string username, password;
        try
        {
            var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader["Basic ".Length..]));
            var colon = decoded.IndexOf(':');
            if (colon < 0) throw new FormatException();
            username = decoded[..colon];
            password = decoded[(colon + 1)..];
        }
        catch
        {
            return new AuthOutcome(null, Results.Json(
                new { error = "Malformed Basic Auth header.", code = "AUTH_FAILED" }, statusCode: 401));
        }

        if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
        {
            return new AuthOutcome(null, Results.Json(
                new { error = "Username and password are required.", code = "AUTH_FAILED" }, statusCode: 401));
        }

        // 1a. Login throttle: refuse a bind while this username+IP key is locked out
        // (fixed-window password-spray guard, arch-review P1/UA5). Consulted BELOW the
        // DisableLogin bypass — a login-disabled deployment never binds, so it never throttles.
        var throttle = context.RequestServices.GetRequiredService<LoginThrottle>();
        var remoteIp = context.Connection.RemoteIpAddress?.ToString();
        if (throttle.IsLockedOut(username, remoteIp!))
        {
            return new AuthOutcome(null, Results.Json(
                new { error = "Too many failed authentication attempts. Try again later.", code = "AUTH_THROTTLED" },
                statusCode: 429));
        }

        // 2. LDAP authentication
        var ldapAuth = context.RequestServices.GetRequiredService<ILdapAuthService>();
        var authResult = await ldapAuth.AuthenticateAsync(username, password, context.RequestAborted);
        if (!authResult.Succeeded)
        {
            throttle.RecordFailure(username, remoteIp!);
            return new AuthOutcome(null, Results.Json(
                new { error = LdapAuthFailureMessages.ToMessage(authResult.Failure), code = "AUTH_FAILED" }, statusCode: 401));
        }

        throttle.RecordSuccess(username, remoteIp!);

        // 3. Role resolution
        var roleMapper = context.RequestServices.GetRequiredService<RoleMapper>();
        var mappingResult = await roleMapper.MapGroupsToRolesAsync(authResult.Groups, context.RequestAborted);

        var permittedSiteIds = mappingResult.IsSystemWideDeployment
            ? Array.Empty<string>()
            : mappingResult.PermittedSiteIds.ToArray();

        var resolvedUser = new AuthenticatedUser(
            authResult.Username,
            authResult.DisplayName,
            mappingResult.Roles.ToArray(),
            permittedSiteIds);

        return new AuthOutcome(resolvedUser, null);
    }
}
