namespace ZB.MOM.WW.Auth.Ldap.Internal;

using Novell.Directory.Ldap;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
// Disambiguate: Novell also declares a RemoteCertificateValidationCallback delegate; the seam and
// LdapConnectionOptions.ConfigureRemoteCertificateValidationCallback both use the BCL one.
using RemoteCertificateValidationCallback = System.Net.Security.RemoteCertificateValidationCallback;

/// <summary>
/// Production <see cref="ILdapConnection"/> backed by <c>Novell.Directory.Ldap.LdapConnection</c>.
/// Mirrors the connection/search idioms from ZB.MOM.WW.ScadaBridge.Security.LdapAuthService.
/// </summary>
/// <remarks>
/// TLS server-certificate validation: by default the underlying
/// <c>Novell.Directory.Ldap.NETStandard</c> client validates the server certificate against the OS
/// trust store (it does NOT blind-accept). A caller-supplied
/// <c>RemoteCertificateValidationCallback</c> overrides that default (CA pinning / SAN checks); when
/// none is supplied and <c>allowInsecure</c> is set, validation is bypassed for dev/test only.
/// </remarks>
internal sealed class NovellLdapConnection : ILdapConnection
{
    private readonly LdapConnection _conn;
    private bool _disposed;

    /// <summary>
    /// Builds the connection, wiring a TLS server-certificate validation policy: a supplied
    /// <paramref name="serverCertificateValidationCallback"/> wins; otherwise <paramref name="allowInsecure"/>
    /// bypasses validation (dev/test only); otherwise the OS-trust-store default applies.
    /// </summary>
    public NovellLdapConnection(
        bool allowInsecure = false,
        RemoteCertificateValidationCallback? serverCertificateValidationCallback = null)
    {
        if (serverCertificateValidationCallback is not null)
        {
            var options = new LdapConnectionOptions()
                .ConfigureRemoteCertificateValidationCallback(serverCertificateValidationCallback);
            _conn = new LdapConnection(options);
        }
        else if (allowInsecure)
        {
            // Dev/test only: accept any server certificate. Reachable solely when an operator has set
            // AllowInsecure (rejected for plaintext-without-AllowInsecure by LdapOptionsValidator).
            var options = new LdapConnectionOptions()
                .ConfigureRemoteCertificateValidationCallback((_, _, _, _) => true);
            _conn = new LdapConnection(options);
        }
        else
        {
            // Default: validate against the OS trust store (no blind-accept).
            _conn = new LdapConnection();
        }
    }

    /// <inheritdoc/>
    public void Connect(
        string host,
        int port,
        LdapTransport transport,
        bool allowInsecure,
        int timeoutMs,
        RemoteCertificateValidationCallback? serverCertificateValidationCallback)
    {
        // The TLS-validation policy (allowInsecure / callback) is wired at construction time on the
        // LdapConnectionOptions; the per-call arguments here are accepted for seam symmetry.
        ApplyTimeout(timeoutMs);

        // LDAPS: TLS is negotiated at the TCP-connection level.
        if (transport == LdapTransport.Ldaps)
            _conn.SecureSocketLayer = true;

        _conn.Connect(host, port);

        // StartTLS: connect plaintext first, then upgrade inside the session.
        if (transport == LdapTransport.StartTls)
        {
            _conn.StartTls();

            if (!_conn.Tls)
                throw new InvalidOperationException(
                    "StartTLS upgrade did not produce an encrypted session.");
        }
    }

    /// <inheritdoc/>
    public void Bind(string dn, string password)
        => _conn.Bind(dn, password);

    /// <inheritdoc/>
    public IReadOnlyList<LdapSearchEntry> Search(
        string searchBase,
        string filter,
        IReadOnlyList<string> attributes)
    {
        var results = _conn.Search(
            searchBase,
            LdapConnection.ScopeSub,
            filter,
            attributes.ToArray(),
            typesOnly: false);

        var entries = new List<LdapSearchEntry>();
        while (results.HasMore())
        {
            var entry = results.Next();
            var attrs = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);

            foreach (LdapAttribute attr in entry.GetAttributeSet())
            {
                attrs[attr.Name] = attr.StringValueArray.ToList();
            }

            entries.Add(new LdapSearchEntry(entry.Dn, attrs));
        }

        return entries;
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        if (_conn.Connected)
            _conn.Disconnect();
        _conn.Dispose();
    }

    // -------------------------------------------------------------------------

    private void ApplyTimeout(int timeoutMs)
    {
        if (timeoutMs <= 0)
            return;

        _conn.ConnectionTimeout = timeoutMs;

        // SearchConstraints.TimeLimit is per-operation (ms). SearchConstraints getter
        // returns the live LdapSearchConstraints object (read-only property), but
        // TimeLimit is mutable in-place via the base LdapConstraints type.
        // We then assign it back through the writable Constraints property so
        // Novell picks up the change — mirrors ScadaBridge idiom.
        var constraints = _conn.SearchConstraints;
        constraints.TimeLimit = timeoutMs;
        _conn.Constraints = constraints;
    }
}

/// <summary>
/// Factory that produces fresh <see cref="NovellLdapConnection"/> instances, carrying the TLS
/// server-certificate validation policy (a supplied callback, or an <c>allowInsecure</c> bypass) so
/// it is wired onto each connection at construction time.
/// </summary>
internal sealed class NovellLdapConnectionFactory(
    bool allowInsecure = false,
    RemoteCertificateValidationCallback? serverCertificateValidationCallback = null)
    : ILdapConnectionFactory
{
    public ILdapConnection Create() =>
        new NovellLdapConnection(allowInsecure, serverCertificateValidationCallback);
}
