// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/opts.go in the NATS server Go source.

using System.Runtime.InteropServices;
using System.Threading;

namespace ZB.MOM.NatsNet.Server;

// Global flag for unknown field handling.
internal static class ConfigFlags
{
    private static int _allowUnknownTopLevelField;

    /// <summary>
    /// Sets whether unknown top-level config fields should be allowed.
    /// Mirrors <c>NoErrOnUnknownFields</c> in opts.go.
    /// </summary>
    public static void NoErrOnUnknownFields(bool noError) =>
        Interlocked.Exchange(ref _allowUnknownTopLevelField, noError ? 1 : 0);

    public static bool AllowUnknownTopLevelField =>
        Interlocked.CompareExchange(ref _allowUnknownTopLevelField, 0, 0) != 0;
}

public sealed partial class ServerOptions
{
    /// <summary>
    /// Snapshot of command-line flags, populated during <see cref="ConfigureOptions"/>.
    /// Mirrors <c>FlagSnapshot</c> in opts.go.
    /// </summary>
    public static ServerOptions? FlagSnapshot { get; internal set; }

    /// <summary>
    /// Deep-copies this <see cref="ServerOptions"/> instance.
    /// Mirrors <c>Options.Clone()</c> in opts.go.
    /// </summary>
    public ServerOptions Clone()
    {
        // Start with a shallow memberwise clone.
        var clone = (ServerOptions)MemberwiseClone();

        // Deep-copy reference types that need isolation.
        if (Routes.Count > 0)
            clone.Routes = Routes.Select(u => new Uri(u.ToString())).ToList();

        clone.Cluster = CloneClusterOpts(Cluster);
        clone.Gateway = CloneGatewayOpts(Gateway);
        clone.LeafNode = CloneLeafNodeOpts(LeafNode);
        clone.Websocket = CloneWebsocketOpts(Websocket);
        clone.Mqtt = CloneMqttOpts(Mqtt);

        clone.Tags = [.. Tags];
        clone.Metadata = new Dictionary<string, string>(Metadata);
        clone.TrustedKeys = [.. TrustedKeys];
        clone.JsAccDefaultDomain = new Dictionary<string, string>(JsAccDefaultDomain);
        clone.InConfig = new Dictionary<string, bool>(InConfig);
        clone.InCmdLine = new Dictionary<string, bool>(InCmdLine);
        clone.OperatorJwt = [.. OperatorJwt];
        clone.ResolverPreloads = new Dictionary<string, string>(ResolverPreloads);
        clone.ResolverPinnedAccounts = [.. ResolverPinnedAccounts];

        return clone;
    }

    /// <summary>
    /// Returns the SHA-256 digest of the configuration.
    /// Mirrors <c>Options.ConfigDigest()</c> in opts.go.
    /// </summary>
    public string ConfigDigest() => ConfigDigestValue;

    // -------------------------------------------------------------------------
    // Merge / Baseline
    // -------------------------------------------------------------------------

    /// <summary>
    /// Merges file-based options with command-line flag options.
    /// Flag options override file options where set.
    /// Mirrors <c>MergeOptions</c> in opts.go.
    /// </summary>
    public static ServerOptions MergeOptions(ServerOptions? fileOpts, ServerOptions? flagOpts)
    {
        if (fileOpts == null) return flagOpts ?? new ServerOptions();
        if (flagOpts == null) return fileOpts;

        var opts = fileOpts.Clone();

        if (flagOpts.Port != 0) opts.Port = flagOpts.Port;
        if (!string.IsNullOrEmpty(flagOpts.Host)) opts.Host = flagOpts.Host;
        if (flagOpts.DontListen) opts.DontListen = true;
        if (!string.IsNullOrEmpty(flagOpts.ClientAdvertise)) opts.ClientAdvertise = flagOpts.ClientAdvertise;
        if (!string.IsNullOrEmpty(flagOpts.Username)) opts.Username = flagOpts.Username;
        if (!string.IsNullOrEmpty(flagOpts.Password)) opts.Password = flagOpts.Password;
        if (!string.IsNullOrEmpty(flagOpts.Authorization)) opts.Authorization = flagOpts.Authorization;
        if (flagOpts.HttpPort != 0) opts.HttpPort = flagOpts.HttpPort;
        if (!string.IsNullOrEmpty(flagOpts.HttpBasePath)) opts.HttpBasePath = flagOpts.HttpBasePath;
        if (flagOpts.Debug) opts.Debug = true;
        if (flagOpts.Trace) opts.Trace = true;
        if (flagOpts.Logtime) opts.Logtime = true;
        if (!string.IsNullOrEmpty(flagOpts.LogFile)) opts.LogFile = flagOpts.LogFile;
        if (!string.IsNullOrEmpty(flagOpts.PidFile)) opts.PidFile = flagOpts.PidFile;
        if (!string.IsNullOrEmpty(flagOpts.PortsFileDir)) opts.PortsFileDir = flagOpts.PortsFileDir;
        if (flagOpts.ProfPort != 0) opts.ProfPort = flagOpts.ProfPort;
        if (!string.IsNullOrEmpty(flagOpts.Cluster.ListenStr)) opts.Cluster.ListenStr = flagOpts.Cluster.ListenStr;
        if (flagOpts.Cluster.NoAdvertise) opts.Cluster.NoAdvertise = true;
        if (flagOpts.Cluster.ConnectRetries != 0) opts.Cluster.ConnectRetries = flagOpts.Cluster.ConnectRetries;
        if (!string.IsNullOrEmpty(flagOpts.Cluster.Advertise)) opts.Cluster.Advertise = flagOpts.Cluster.Advertise;
        if (!string.IsNullOrEmpty(flagOpts.RoutesStr)) MergeRoutes(opts, flagOpts);
        if (flagOpts.JetStream) opts.JetStream = true;
        if (!string.IsNullOrEmpty(flagOpts.StoreDir)) opts.StoreDir = flagOpts.StoreDir;

        return opts;
    }

    /// <summary>
    /// Parses route URLs from a comma-separated string.
    /// Mirrors <c>RoutesFromStr</c> in opts.go.
    /// </summary>
    public static List<Uri> RoutesFromStr(string routesStr)
    {
        var parts = routesStr.Split(',');
        if (parts.Length == 0) return [];

        var urls = new List<Uri>();
        foreach (var r in parts)
        {
            var trimmed = r.Trim();
            if (Uri.TryCreate(trimmed, UriKind.Absolute, out var u))
                urls.Add(u);
        }
        return urls;
    }

    /// <summary>
    /// Applies system-wide defaults to any unset options.
    /// Mirrors <c>setBaselineOptions</c> in opts.go.
    /// </summary>
    public void SetBaselineOptions()
    {
        if (string.IsNullOrEmpty(Host))
            Host = ServerConstants.DefaultHost;
        if (string.IsNullOrEmpty(HttpHost))
            HttpHost = Host;
        if (Port == 0)
            Port = ServerConstants.DefaultPort;
        else if (Port == ServerConstants.RandomPort)
            Port = 0;
        if (MaxConn == 0)
            MaxConn = ServerConstants.DefaultMaxConnections;
        if (PingInterval == TimeSpan.Zero)
            PingInterval = ServerConstants.DefaultPingInterval;
        if (MaxPingsOut == 0)
            MaxPingsOut = ServerConstants.DefaultPingMaxOut;
        if (TlsTimeout == 0)
            TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
        if (AuthTimeout == 0)
            AuthTimeout = GetDefaultAuthTimeout(TlsConfig, TlsTimeout);

        // Cluster defaults
        if (Cluster.Port != 0 || !string.IsNullOrEmpty(Cluster.ListenStr))
        {
            if (string.IsNullOrEmpty(Cluster.Host))
                Cluster.Host = ServerConstants.DefaultHost;
            if (Cluster.TlsTimeout == 0)
                Cluster.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
            if (Cluster.AuthTimeout == 0)
                Cluster.AuthTimeout = GetDefaultAuthTimeout(Cluster.TlsConfig, Cluster.TlsTimeout);
            if (Cluster.PoolSize == 0)
                Cluster.PoolSize = ServerConstants.DefaultRoutePoolSize;

            // Add system account to pinned accounts if pool is enabled.
            if (Cluster.PoolSize > 0)
            {
                var sysAccName = SystemAccount;
                if (string.IsNullOrEmpty(sysAccName) && !NoSystemAccount)
                    sysAccName = ServerConstants.DefaultSystemAccount;
                if (!string.IsNullOrEmpty(sysAccName) && !Cluster.PinnedAccounts.Contains(sysAccName))
                    Cluster.PinnedAccounts.Add(sysAccName);
            }

            // Default compression to "accept".
            if (string.IsNullOrEmpty(Cluster.Compression.Mode))
                Cluster.Compression.Mode = CompressionModes.Accept;
        }

        // LeafNode defaults
        if (LeafNode.Port != 0)
        {
            if (string.IsNullOrEmpty(LeafNode.Host))
                LeafNode.Host = ServerConstants.DefaultHost;
            if (LeafNode.TlsTimeout == 0)
                LeafNode.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
            if (LeafNode.AuthTimeout == 0)
                LeafNode.AuthTimeout = GetDefaultAuthTimeout(LeafNode.TlsConfig, LeafNode.TlsTimeout);
            if (string.IsNullOrEmpty(LeafNode.Compression.Mode))
                LeafNode.Compression.Mode = CompressionModes.S2Auto;
        }

        // Remote leafnode defaults
        foreach (var r in LeafNode.Remotes)
        {
            foreach (var u in r.Urls)
            {
                if (u.IsDefaultPort || string.IsNullOrEmpty(u.GetComponents(UriComponents.Port, UriFormat.Unescaped)))
                {
                    var builder = new UriBuilder(u) { Port = ServerConstants.DefaultLeafNodePort };
                    r.Urls[r.Urls.IndexOf(u)] = builder.Uri;
                }
            }
            if (string.IsNullOrEmpty(r.Compression.Mode))
                r.Compression.Mode = CompressionModes.S2Auto;
            if (r.FirstInfoTimeout <= TimeSpan.Zero)
                r.FirstInfoTimeout = ServerConstants.DefaultLeafNodeInfoWait;
        }

        if (LeafNode.ReconnectInterval == TimeSpan.Zero)
            LeafNode.ReconnectInterval = ServerConstants.DefaultLeafNodeReconnect;

        // Protocol limits
        if (MaxControlLine == 0)
            MaxControlLine = ServerConstants.MaxControlLineSize;
        if (MaxPayload == 0)
            MaxPayload = ServerConstants.MaxPayloadSize;
        if (MaxPending == 0)
            MaxPending = ServerConstants.MaxPendingSize;
        if (WriteDeadline == TimeSpan.Zero)
            WriteDeadline = ServerConstants.DefaultFlushDeadline;
        if (MaxClosedClients == 0)
            MaxClosedClients = ServerConstants.DefaultMaxClosedClients;
        if (LameDuckDuration == TimeSpan.Zero)
            LameDuckDuration = ServerConstants.DefaultLameDuckDuration;
        if (LameDuckGracePeriod == TimeSpan.Zero)
            LameDuckGracePeriod = ServerConstants.DefaultLameDuckGracePeriod;

        // Gateway defaults
        if (Gateway.Port != 0)
        {
            if (string.IsNullOrEmpty(Gateway.Host))
                Gateway.Host = ServerConstants.DefaultHost;
            if (Gateway.TlsTimeout == 0)
                Gateway.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
            if (Gateway.AuthTimeout == 0)
                Gateway.AuthTimeout = GetDefaultAuthTimeout(Gateway.TlsConfig, Gateway.TlsTimeout);
        }

        // Error reporting
        if (ConnectErrorReports == 0)
            ConnectErrorReports = ServerConstants.DefaultConnectErrorReports;
        if (ReconnectErrorReports == 0)
            ReconnectErrorReports = ServerConstants.DefaultReconnectErrorReports;

        // WebSocket defaults
        if (Websocket.Port != 0)
        {
            if (string.IsNullOrEmpty(Websocket.Host))
                Websocket.Host = ServerConstants.DefaultHost;
        }

        // MQTT defaults
        if (Mqtt.Port != 0)
        {
            if (string.IsNullOrEmpty(Mqtt.Host))
                Mqtt.Host = ServerConstants.DefaultHost;
            if (Mqtt.TlsTimeout == 0)
                Mqtt.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds;
        }

        // JetStream defaults
        if (JetStreamMaxMemory == 0 && !MaxMemSet)
            JetStreamMaxMemory = -1;
        if (JetStreamMaxStore == 0 && !MaxStoreSet)
            JetStreamMaxStore = -1;
        if (SyncInterval == TimeSpan.Zero && !SyncSet)
            SyncInterval = TimeSpan.FromMinutes(2); // defaultSyncInterval
        if (JetStreamRequestQueueLimit <= 0)
            JetStreamRequestQueueLimit = 4096; // JSDefaultRequestQueueLimit
    }

    /// <summary>
    /// Normalizes an HTTP base path (ensure leading slash, clean redundant separators).
    /// Mirrors <c>normalizeBasePath</c> in opts.go.
    /// </summary>
    public static string NormalizeBasePath(string p)
    {
        if (string.IsNullOrEmpty(p)) return "/";
        if (p[0] != '/') p = "/" + p;
        // Simple path clean: collapse repeated slashes and remove trailing slash.
        while (p.Contains("//")) p = p.Replace("//", "/");
        return p.Length > 1 && p.EndsWith('/') ? p[..^1] : p;
    }

    /// <summary>
    /// Computes the default auth timeout based on TLS config presence.
    /// Mirrors <c>getDefaultAuthTimeout</c> in opts.go.
    /// </summary>
    public static double GetDefaultAuthTimeout(object? tlsConfig, double tlsTimeout)
    {
        if (tlsConfig != null)
            return tlsTimeout + 1.0;
        return ServerConstants.AuthTimeout.TotalSeconds;
    }

    /// <summary>
    /// Returns the user's home directory.
    /// Mirrors <c>homeDir</c> in opts.go.
    /// </summary>
    public static string HomeDir() => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

    /// <summary>
    /// Expands environment variables and ~/ prefix in a path.
    /// Mirrors <c>expandPath</c> in opts.go.
    /// </summary>
    public static string ExpandPath(string p)
    {
        p = Environment.ExpandEnvironmentVariables(p);
        if (!p.StartsWith('~')) return p;
        return Path.Combine(HomeDir(), p[1..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
    }

    /// <summary>
    /// Reads a PID from a file path if possible, otherwise returns the string as-is.
    /// Mirrors <c>maybeReadPidFile</c> in opts.go.
    /// </summary>
    public static string MaybeReadPidFile(string pidStr)
    {
        try { return File.ReadAllText(pidStr).Trim(); }
        catch { return pidStr; }
    }

    /// <summary>
    /// Applies TLS overrides from command-line options.
    /// Mirrors <c>overrideTLS</c> in opts.go.
    /// </summary>
    public Exception? OverrideTls()
    {
        if (string.IsNullOrEmpty(TlsCert))
            return new InvalidOperationException("TLS Server certificate must be present and valid");
        if (string.IsNullOrEmpty(TlsKey))
            return new InvalidOperationException("TLS Server private key must be present and valid");

        // TLS config generation is deferred to GenTlsConfig (session 06+).
        // For now, mark that TLS is enabled.
        Tls = true;
        return null;
    }

    /// <summary>
    /// Overrides cluster options from the --cluster flag.
    /// Mirrors <c>overrideCluster</c> in opts.go.
    /// </summary>
    public Exception? OverrideCluster()
    {
        if (string.IsNullOrEmpty(Cluster.ListenStr))
        {
            Cluster.Port = 0;
            return null;
        }

        var listenStr = Cluster.ListenStr;
        var wantsRandom = false;
        if (listenStr.EndsWith(":-1"))
        {
            wantsRandom = true;
            listenStr = listenStr[..^3] + ":0";
        }

        if (!Uri.TryCreate(listenStr, UriKind.Absolute, out var clusterUri))
            return new InvalidOperationException($"could not parse cluster URL: {Cluster.ListenStr}");

        Cluster.Host = clusterUri.Host;
        Cluster.Port = wantsRandom ? -1 : clusterUri.Port;

        var userInfo = clusterUri.UserInfo;
        if (!string.IsNullOrEmpty(userInfo))
        {
            var parts = userInfo.Split(':', 2);
            if (parts.Length != 2)
                return new InvalidOperationException("expected cluster password to be set");
            Cluster.Username = parts[0];
            Cluster.Password = parts[1];
        }
        else
        {
            Cluster.Username = string.Empty;
            Cluster.Password = string.Empty;
        }

        return null;
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    private static void MergeRoutes(ServerOptions opts, ServerOptions flagOpts)
    {
        var routeUrls = RoutesFromStr(flagOpts.RoutesStr);
        if (routeUrls.Count == 0) return;
        opts.Routes = routeUrls;
        opts.RoutesStr = flagOpts.RoutesStr;
    }

    internal static void TrackExplicitVal(Dictionary<string, bool> pm, string name, bool val) =>
        pm[name] = val;

    private static ClusterOpts CloneClusterOpts(ClusterOpts src) => new()
    {
        Name = src.Name,
        Host = src.Host,
        Port = src.Port,
        Username = src.Username,
        Password = src.Password,
        AuthTimeout = src.AuthTimeout,
        TlsTimeout = src.TlsTimeout,
        TlsConfig = src.TlsConfig,
        TlsMap = src.TlsMap,
        TlsCheckKnownUrls = src.TlsCheckKnownUrls,
        TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
        TlsHandshakeFirst = src.TlsHandshakeFirst,
        TlsHandshakeFirstFallback = src.TlsHandshakeFirstFallback,
        ListenStr = src.ListenStr,
        Advertise = src.Advertise,
        NoAdvertise = src.NoAdvertise,
        ConnectRetries = src.ConnectRetries,
        ConnectBackoff = src.ConnectBackoff,
        PoolSize = src.PoolSize,
        PinnedAccounts = [.. src.PinnedAccounts],
        Compression = new CompressionOpts { Mode = src.Compression.Mode, RttThresholds = [.. src.Compression.RttThresholds] },
        PingInterval = src.PingInterval,
        MaxPingsOut = src.MaxPingsOut,
        WriteDeadline = src.WriteDeadline,
        WriteTimeout = src.WriteTimeout,
    };

    private static GatewayOpts CloneGatewayOpts(GatewayOpts src) => new()
    {
        Name = src.Name,
        Host = src.Host,
        Port = src.Port,
        Username = src.Username,
        Password = src.Password,
        AuthTimeout = src.AuthTimeout,
        TlsConfig = src.TlsConfig,
        TlsTimeout = src.TlsTimeout,
        TlsMap = src.TlsMap,
        TlsCheckKnownUrls = src.TlsCheckKnownUrls,
        TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
        Advertise = src.Advertise,
        ConnectRetries = src.ConnectRetries,
        ConnectBackoff = src.ConnectBackoff,
        Gateways = src.Gateways.Select(g => new RemoteGatewayOpts
        {
            Name = g.Name,
            TlsConfig = g.TlsConfig,
            TlsTimeout = g.TlsTimeout,
            Urls = g.Urls.Select(u => new Uri(u.ToString())).ToList(),
        }).ToList(),
        RejectUnknown = src.RejectUnknown,
        WriteDeadline = src.WriteDeadline,
        WriteTimeout = src.WriteTimeout,
    };

    private static LeafNodeOpts CloneLeafNodeOpts(LeafNodeOpts src) => new()
    {
        Host = src.Host,
        Port = src.Port,
        Username = src.Username,
        Password = src.Password,
        ProxyRequired = src.ProxyRequired,
        Nkey = src.Nkey,
        Account = src.Account,
        AuthTimeout = src.AuthTimeout,
        TlsConfig = src.TlsConfig,
        TlsTimeout = src.TlsTimeout,
        TlsMap = src.TlsMap,
        TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
        TlsHandshakeFirst = src.TlsHandshakeFirst,
        TlsHandshakeFirstFallback = src.TlsHandshakeFirstFallback,
        Advertise = src.Advertise,
        NoAdvertise = src.NoAdvertise,
        ReconnectInterval = src.ReconnectInterval,
        WriteDeadline = src.WriteDeadline,
        WriteTimeout = src.WriteTimeout,
        Compression = new CompressionOpts { Mode = src.Compression.Mode, RttThresholds = [.. src.Compression.RttThresholds] },
        Remotes = src.Remotes.Select(r => new RemoteLeafOpts
        {
            LocalAccount = r.LocalAccount,
            NoRandomize = r.NoRandomize,
            Urls = r.Urls.Select(u => new Uri(u.ToString())).ToList(),
            Credentials = r.Credentials,
            Nkey = r.Nkey,
            Tls = r.Tls,
            TlsTimeout = r.TlsTimeout,
            TlsHandshakeFirst = r.TlsHandshakeFirst,
            Hub = r.Hub,
            DenyImports = [.. r.DenyImports],
            DenyExports = [.. r.DenyExports],
            FirstInfoTimeout = r.FirstInfoTimeout,
            Compression = new CompressionOpts { Mode = r.Compression.Mode, RttThresholds = [.. r.Compression.RttThresholds] },
            JetStreamClusterMigrate = r.JetStreamClusterMigrate,
            JetStreamClusterMigrateDelay = r.JetStreamClusterMigrateDelay,
            LocalIsolation = r.LocalIsolation,
            RequestIsolation = r.RequestIsolation,
            Disabled = r.Disabled,
        }).ToList(),
        MinVersion = src.MinVersion,
        IsolateLeafnodeInterest = src.IsolateLeafnodeInterest,
    };

    private static WebsocketOpts CloneWebsocketOpts(WebsocketOpts src) => new()
    {
        Host = src.Host,
        Port = src.Port,
        Advertise = src.Advertise,
        NoAuthUser = src.NoAuthUser,
        JwtCookie = src.JwtCookie,
        UsernameCookie = src.UsernameCookie,
        PasswordCookie = src.PasswordCookie,
        TokenCookie = src.TokenCookie,
        Username = src.Username,
        Password = src.Password,
        Token = src.Token,
        AuthTimeout = src.AuthTimeout,
        NoTls = src.NoTls,
        TlsConfig = src.TlsConfig,
        TlsMap = src.TlsMap,
        TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
        SameOrigin = src.SameOrigin,
        AllowedOrigins = [.. src.AllowedOrigins],
        Compression = src.Compression,
        HandshakeTimeout = src.HandshakeTimeout,
        PingInterval = src.PingInterval,
        Headers = new Dictionary<string, string>(src.Headers),
    };

    private static MqttOpts CloneMqttOpts(MqttOpts src) => new()
    {
        Host = src.Host,
        Port = src.Port,
        NoAuthUser = src.NoAuthUser,
        Username = src.Username,
        Password = src.Password,
        Token = src.Token,
        JsDomain = src.JsDomain,
        StreamReplicas = src.StreamReplicas,
        ConsumerReplicas = src.ConsumerReplicas,
        ConsumerMemoryStorage = src.ConsumerMemoryStorage,
        ConsumerInactiveThreshold = src.ConsumerInactiveThreshold,
        AuthTimeout = src.AuthTimeout,
        TlsConfig = src.TlsConfig,
        TlsMap = src.TlsMap,
        TlsTimeout = src.TlsTimeout,
        TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null,
        AckWait = src.AckWait,
        JsApiTimeout = src.JsApiTimeout,
        MaxAckPending = src.MaxAckPending,
    };
}
