using ZB.MOM.WW.Configuration;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport;

namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;

/// <summary>
///     Fail-fast startup validator for <see cref="LdapOptions"/>, built on the shared
///     <c>ZB.MOM.WW.Configuration</c> <see cref="OptionsValidatorBase{TOptions}"/>. When LDAP login
///     is enabled, <c>Server</c> and <c>SearchBase</c> must be set and <c>Port</c> must be a valid
///     TCP port; when disabled — or when <c>DevStubMode</c> bypasses the real bind — all checks are
///     skipped. <c>ServiceAccountDn</c>/<c>Password</c> are
///     intentionally not required — an empty pair selects the direct-bind path (see
///     <see cref="LdapOptions.ServiceAccountDn"/>). Failure messages use <c>"Ldap:"</c> as a
///     human-readable field prefix — not the literal bound section path, which is
///     <c>Security:Ldap</c> (see <see cref="LdapOptions.SectionName"/>).
/// </summary>
/// <remarks>
///     Insecure-transport guard (review fix): a real-LDAP config that selects plaintext transport
///     (<see cref="LdapTransport.None"/>) without opting in via <see cref="LdapOptions.AllowInsecure"/>
///     now FAILS startup validation, so an insecure-by-accident production overlay never boots.
///     This mirrors the login-time fail-closed guard in <see cref="OtOpcUaLdapAuthService"/> and is
///     gated on the same conditions (<see cref="LdapOptions.Enabled"/> AND not
///     <see cref="LdapOptions.DevStubMode"/>): a disabled or dev-stub config is exempt, exactly as it
///     is exempt from the real bind. The login-time guard remains as defence in depth.
/// </remarks>
public sealed class LdapOptionsValidator : OptionsValidatorBase<LdapOptions>
{
    /// <inheritdoc />
    protected override void Validate(ValidationBuilder builder, LdapOptions options)
    {
        // Skip the real-LDAP field checks when LDAP login is disabled, or when the dev stub is
        // active — DevStubMode bypasses the real bind entirely, so Server/SearchBase/Port are
        // irrelevant and would otherwise force dev configs to carry meaningless placeholders.
        if (!options.Enabled || options.DevStubMode) return;

        builder.RequireThat(!string.IsNullOrWhiteSpace(options.Server),
            "Ldap:Server is required when LDAP login is enabled.");
        builder.RequireThat(!string.IsNullOrWhiteSpace(options.SearchBase),
            "Ldap:SearchBase is required when LDAP login is enabled.");
        builder.Port(options.Port, "Ldap:Port");

        // Fail closed at startup on a plaintext transport unless explicitly opted in — same
        // condition the login-time guard in OtOpcUaLdapAuthService enforces, lifted to boot so an
        // insecure-by-accident production overlay refuses to start rather than silently failing
        // every bind at login.
        builder.RequireThat(
            !(options.Transport == LdapTransport.None && !options.AllowInsecure),
            "LDAP transport is None (plaintext) but AllowInsecure is false — set Transport to Ldaps/StartTls or set AllowInsecure for dev.");

        // S2 resilience knobs — a non-positive timeout / concurrency bound would silently disable the
        // wall-clock/concurrency protection the OPC UA data-plane authenticator relies on, so fail fast.
        builder.RequireThat(options.ConnectionTimeoutMs > 0,
            "Ldap:ConnectionTimeoutMs must be > 0.");
        builder.RequireThat(options.AuthTimeoutMs > 0,
            "Ldap:AuthTimeoutMs must be > 0.");
        builder.RequireThat(options.MaxConcurrentAuthentications > 0,
            "Ldap:MaxConcurrentAuthentications must be > 0.");
        // An open circuit with no cooldown would re-probe LDAP on every call — pointless. Only enforce
        // a positive cooldown when the circuit is actually enabled (OutageFailureThreshold > 0; 0 disables).
        builder.RequireThat(options.OutageFailureThreshold <= 0 || options.OutageCooldownSeconds > 0,
            "Ldap:OutageCooldownSeconds must be > 0 when Ldap:OutageFailureThreshold > 0 (0 disables the circuit).");
    }
}
