using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using LibLdapAuthResult = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapAuthResult;
using LibLdapAuthService = ZB.MOM.WW.Auth.Ldap.LdapAuthService;
using LibILdapAuthService = ZB.MOM.WW.Auth.Abstractions.Ldap.ILdapAuthService;

namespace ZB.MOM.WW.OtOpcUa.Security.Ldap;

/// <summary>
///     OtOpcUa's application <see cref="ILdapAuthService"/> — a thin wrapper around the shared
///     <c>ZB.MOM.WW.Auth.Ldap</c> directory client that adds the two app-only concerns the shared
///     library deliberately does not model:
///     <list type="number">
///         <item>the <see cref="LdapOptions.Enabled"/> master switch (disabled ⇒ deny, no bind); and</item>
///         <item><see cref="LdapOptions.DevStubMode"/> — the dev bypass that grants an Administrator
///             session WITHOUT touching the network, so an operator can navigate the full Admin UI
///             against a machine with no directory.</item>
///     </list>
///     On the real path it delegates to the library <see cref="LibLdapAuthService"/> and adapts the
///     library result (which returns <em>groups</em>, never roles) back onto the app's
///     <see cref="LdapAuthResult"/> shape. Role resolution itself now lives downstream in
///     <see cref="OtOpcUaGroupRoleMapper"/> (the <c>IGroupRoleMapper&lt;string&gt;</c> seam), which
///     both the login endpoint and the OPC UA data-plane authenticator call with the returned
///     <see cref="LdapAuthResult.Groups"/>. The only path that pre-populates
///     <see cref="LdapAuthResult.Roles"/> is the DevStub success; consumers union that pre-resolved
///     set with the mapper output so the dev Administrator grant survives the move to the mapper.
/// </summary>
/// <remarks>
///     Fail-closed: the library never throws, and this wrapper adds no new throwing paths. The
///     DevStub result grants the canonical <c>"Administrator"</c> control-plane role (group
///     <c>"dev"</c>) so the dev session can navigate the full Admin UI (renamed from the prior
///     <c>"FleetAdmin"</c> to the canonical <c>"Administrator"</c>).
/// </remarks>
public sealed class OtOpcUaLdapAuthService : ILdapAuthService
{
    private readonly LdapOptions _options;
    private readonly LibILdapAuthService _inner;
    private readonly ILogger<OtOpcUaLdapAuthService> _logger;

    /// <summary>
    ///     Production constructor: builds the shared-library directory client from the wire fields
    ///     of the bound app <see cref="LdapOptions"/>.
    /// </summary>
    /// <param name="options">The app LDAP options (wire fields + app-only concerns).</param>
    /// <param name="logger">The logger.</param>
    public OtOpcUaLdapAuthService(IOptions<LdapOptions> options, ILogger<OtOpcUaLdapAuthService> logger)
        : this(options.Value, new LibLdapAuthService(options.Value.ToLibraryOptions()), logger)
    {
    }

    /// <summary>
    ///     Seam constructor: accepts an injected library <see cref="LibILdapAuthService"/> so the
    ///     Enabled/DevStub/delegation logic can be unit-tested without a live directory.
    /// </summary>
    /// <param name="options">The app LDAP options.</param>
    /// <param name="inner">The shared-library directory client to delegate the real path to.</param>
    /// <param name="logger">The logger.</param>
    internal OtOpcUaLdapAuthService(LdapOptions options, LibILdapAuthService inner, ILogger<OtOpcUaLdapAuthService> logger)
    {
        _options = options;
        _inner = inner;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<LdapAuthResult> AuthenticateAsync(string username, string password, CancellationToken ct = default)
    {
        // Enabled is the master switch and wins over DevStubMode — when LDAP auth is turned off,
        // refuse to authenticate at all (no bind, no dev-stub bypass).
        if (!_options.Enabled)
            return new(false, null, null, [], [], "LDAP authentication is disabled.");

        if (string.IsNullOrWhiteSpace(username))
            return new(false, null, null, [], [], "Username is required");
        if (string.IsNullOrWhiteSpace(password))
            return new(false, null, username, [], [], "Password is required");

        if (_options.DevStubMode)
        {
            // Dev bypass: accept any non-empty credentials and grant Administrator WITHOUT a real bind.
            // Pre-populated Roles are unioned with the mapper output by both consumers, so the grant
            // survives the move to IGroupRoleMapper. (The role string was canonicalized from the
            // prior "FleetAdmin" to "Administrator".)
            _logger.LogWarning(
                "OtOpcUaLdapAuthService: DevStubMode bypass — accepting {User} without a real LDAP bind", username);
            return new(true, username, username, ["dev"], ["Administrator"], null);
        }

        // Fail closed on a plaintext transport unless explicitly opted in. The bespoke service
        // enforced this at login (not startup), so the host still boots with an insecure-by-default
        // config and only refuses the bind here — preserved verbatim after the UseTls→Transport
        // migration. The shared library's directory client does not re-check this.
        if (_options.Transport == LdapTransport.None && !_options.AllowInsecure)
            return new(false, null, username, [], [],
                "Insecure LDAP is disabled. Enable a TLS transport or set AllowInsecure for dev/test.");

        var libResult = await _inner.AuthenticateAsync(username, password, ct).ConfigureAwait(false);
        return Adapt(libResult, username);
    }

    /// <summary>
    ///     Maps the shared-library <see cref="LibLdapAuthResult"/> onto the app's
    ///     <see cref="LdapAuthResult"/>. The library returns groups (never roles) on success, so
    ///     <see cref="LdapAuthResult.Roles"/> is left empty on the delegated path — role resolution
    ///     happens downstream in the mapper. Library <see cref="LdapAuthFailure"/> codes are folded
    ///     into the user-facing error strings the app already surfaces, keeping fail-closed semantics.
    /// </summary>
    /// <param name="result">The library authentication result.</param>
    /// <param name="username">The login name, used to populate the app result's Username field.</param>
    private static LdapAuthResult Adapt(LibLdapAuthResult result, string username)
    {
        if (result.Succeeded)
            return new(true, result.DisplayName, result.Username, result.Groups, [], null);

        // ServiceAccountBindFailed is the library's directory-unreachable / service-account-misconfigured
        // bucket (its enum has no dedicated DirectoryUnavailable) — flag it as system-side so the OPC UA
        // authenticator's outage circuit can distinguish an outage from a user-credential deny (03/S2).
        return new(false, null, username, [], [], FailureToError(result.Failure))
        {
            IsSystemFailure = result.Failure is LdapAuthFailure.ServiceAccountBindFailed,
        };
    }

    /// <summary>Folds a structured library failure code into the app's user-facing error text.</summary>
    /// <param name="failure">The library failure code (null defensively treated as a generic error).</param>
    private static string FailureToError(LdapAuthFailure? failure) => failure switch
    {
        // The directory found no single matching user, or the password did not verify — both
        // surface as the same opaque message so a probe cannot distinguish "unknown user" from
        // "wrong password".
        LdapAuthFailure.BadCredentials => "Invalid username or password",
        LdapAuthFailure.UserNotFound => "Invalid username or password",
        LdapAuthFailure.AmbiguousUser => "Invalid username or password",
        LdapAuthFailure.GroupLookupFailed => "Invalid username or password",
        // System-side faults (directory unreachable / service-account misconfiguration) — kept as a
        // generic backend message rather than leaking the cause to the caller.
        LdapAuthFailure.ServiceAccountBindFailed => "Unexpected authentication error",
        LdapAuthFailure.Disabled => "LDAP authentication is disabled.",
        _ => "Unexpected authentication error",
    };
}
