using ZB.MOM.WW.Auth.Abstractions.Ldap;
using LibLdapOptions = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapOptions;

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

/// <summary>
///     LDAP + role-mapping configuration for the Admin UI. Bound from <c>appsettings.json</c>
///     <c>Security:Ldap</c> section. Defaults point at the shared GLAuth dev instance on the
///     Linux Docker host (<c>10.100.0.35:3893</c>) — see <c>docs/security.md</c> §"LDAP bind
///     flow" and <c>CLAUDE.md</c> §"LDAP Authentication" for setup details.
/// </summary>
/// <remarks>
///     Carries both the wire fields the shared <c>ZB.MOM.WW.Auth.Ldap</c> directory client needs
///     (<see cref="Server"/>/<see cref="Port"/>/<see cref="Transport"/>/…) AND the app-only concerns
///     the shared library has no notion of (<see cref="Enabled"/> master switch,
///     <see cref="DevStubMode"/> dev bypass, <see cref="GroupToRole"/> appsettings role baseline).
///     The app wrapper (<c>OtOpcUaLdapAuthService</c>) projects this onto the library's
///     <see cref="LibLdapOptions"/> at construction; see <see cref="ToLibraryOptions"/>.
/// </remarks>
public sealed class LdapOptions
{
    public const string SectionName = "Security:Ldap";

    /// <summary>Gets or sets a value indicating whether LDAP authentication is enabled.</summary>
    public bool Enabled { get; set; } = true;

    /// <summary>Gets or sets the LDAP server hostname.</summary>
    public string Server { get; set; } = "localhost";

    /// <summary>Gets or sets the LDAP server port.</summary>
    public int Port { get; set; } = 3893;

    /// <summary>
    ///     Transport security for the LDAP connection — <see cref="LdapTransport.Ldaps"/> (implicit
    ///     TLS), <see cref="LdapTransport.StartTls"/> (upgrade), or <see cref="LdapTransport.None"/>
    ///     (plaintext, dev/test only — requires <see cref="AllowInsecure"/>). Replaces the former
    ///     <c>UseTls</c> bool: <c>true</c>→<see cref="LdapTransport.Ldaps"/>,
    ///     <c>false</c>→<see cref="LdapTransport.None"/>.
    /// </summary>
    public LdapTransport Transport { get; set; } = LdapTransport.None;

    /// <summary>Dev-only escape hatch — must be <c>false</c> in production. Maps to the shared
    ///     library's <see cref="LibLdapOptions.AllowInsecure"/> (renamed from <c>AllowInsecureLdap</c>).</summary>
    public bool AllowInsecure { get; set; }

    /// <summary>
    ///     Dev-only stub: when <c>true</c>, <see cref="OtOpcUaLdapAuthService"/> bypasses the real LDAP
    ///     bind and accepts any non-empty username/password, returning a single Administrator role
    ///     so the operator can navigate the full Admin UI. MUST be <c>false</c> in production.
    /// </summary>
    public bool DevStubMode { get; set; }

    /// <summary>Gets or sets the LDAP search base DN.</summary>
    public string SearchBase { get; set; } = "dc=zb,dc=local";

    /// <summary>
    ///     Service-account DN used for search-then-bind. When empty, a direct-bind with
    ///     <c>cn={user},{SearchBase}</c> is attempted.
    /// </summary>
    public string ServiceAccountDn { get; set; } = string.Empty;

    /// <summary>Gets or sets the service account password for LDAP authentication.</summary>
    public string ServiceAccountPassword { get; set; } = string.Empty;

    /// <summary>Gets or sets the LDAP attribute name for user display name.</summary>
    public string DisplayNameAttribute { get; set; } = "cn";

    /// <summary>Gets or sets the LDAP attribute name for group membership.</summary>
    public string GroupAttribute { get; set; } = "memberOf";

    /// <summary>
    ///     Attribute the service-account search matches the login name against to resolve the
    ///     user's DN. <c>cn</c> for GLAuth (the dev default); set <c>sAMAccountName</c> for
    ///     Active Directory.
    /// </summary>
    public string UserNameAttribute { get; set; } = "cn";

    /// <summary>
    ///     Maps LDAP group name → Admin role. Group match is case-insensitive. A user gets every
    ///     role whose source group is in their membership list. Values are the canonical control-plane
    ///     roles. Example dev mapping:
    ///     <code>"ReadOnly":"Viewer","ReadWrite":"Designer","AlarmAck":"Administrator"</code>
    /// </summary>
    public Dictionary<string, string> GroupToRole { get; set; } = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>
    ///     Bounds (in milliseconds) the shared-library directory client's socket connect AND each
    ///     per-operation search inside a single bind flow. Projected into the library's
    ///     <see cref="LibLdapOptions.ConnectionTimeoutMs"/> via <see cref="ToLibraryOptions"/>. The
    ///     default (10000) matches the library default, so behaviour is unchanged unless configured.
    ///     This is the FIRST line of defence against a directory outage; the whole-flow
    ///     <see cref="AuthTimeoutMs"/> is the backstop (03/S2).
    /// </summary>
    public int ConnectionTimeoutMs { get; set; } = 10_000;

    /// <summary>
    ///     Hard whole-flow wall-clock bound (in milliseconds) the OPC UA data-plane authenticator
    ///     (<c>LdapOpcUaUserAuthenticator</c>) applies across the ENTIRE authenticate flow — bind plus
    ///     role-map — so the OPC UA SDK's synchronous impersonation callback (raised under a
    ///     <c>SessionManager</c>-wide event lock) can never be wedged by a slow / hung directory.
    ///     Deliberately &gt; <see cref="ConnectionTimeoutMs"/> (default 15000) so a healthy-but-slow
    ///     full flow is not cut off while an outage is bounded by the library timeout first (03/S2).
    /// </summary>
    public int AuthTimeoutMs { get; set; } = 15_000;

    /// <summary>
    ///     Maximum number of in-flight OPC UA authentication flows the data-plane authenticator admits
    ///     at once. A caller that cannot acquire a slot within its budget is denied "backend busy"
    ///     (local backpressure, not counted against the outage circuit). Bounds accumulation of
    ///     orphaned / timed-out bind tasks during an outage (03/S2). Default 8.
    /// </summary>
    public int MaxConcurrentAuthentications { get; set; } = 8;

    /// <summary>
    ///     Number of CONSECUTIVE system-side failures (boundary timeout, directory-unreachable result,
    ///     or unexpected throw) after which the data-plane authenticator's outage circuit opens and
    ///     denies instantly for <see cref="OutageCooldownSeconds"/> without touching LDAP. Any success
    ///     or user-side (credential) deny resets the counter. <c>0</c> disables the circuit (03/S2).
    ///     Default 3.
    /// </summary>
    public int OutageFailureThreshold { get; set; } = 3;

    /// <summary>
    ///     How long (in seconds) the outage circuit stays open before a half-open probe is allowed
    ///     through. Only meaningful when <see cref="OutageFailureThreshold"/> &gt; 0. Default 15 (03/S2).
    /// </summary>
    public int OutageCooldownSeconds { get; set; } = 15;

    /// <summary>
    ///     Projects the wire fields onto the shared <c>ZB.MOM.WW.Auth.Ldap</c>
    ///     <see cref="LibLdapOptions"/> the directory client consumes. App-only concerns
    ///     (<see cref="DevStubMode"/>, <see cref="GroupToRole"/>) have no library counterpart and are
    ///     handled by the app wrapper around the library service; <see cref="Enabled"/> is carried
    ///     through so the library's own feature gate stays consistent with the app master switch.
    /// </summary>
    /// <returns>The projected <see cref="LibLdapOptions"/> for the shared LDAP client library.</returns>
    public LibLdapOptions ToLibraryOptions() => new()
    {
        Enabled = Enabled,
        Server = Server,
        Port = Port,
        Transport = Transport,
        AllowInsecure = AllowInsecure,
        SearchBase = SearchBase,
        ServiceAccountDn = ServiceAccountDn,
        ServiceAccountPassword = ServiceAccountPassword,
        UserNameAttribute = UserNameAttribute,
        DisplayNameAttribute = DisplayNameAttribute,
        GroupAttribute = GroupAttribute,
        ConnectionTimeoutMs = ConnectionTimeoutMs,
    };
}
