// Copyright 2012-2026 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/server.go (lines 2779–4407) in the NATS server Go source.
// Session 10: accept loops, client creation, connect URL management, port helpers.

using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text.Json;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server;

public sealed partial class NatsServer
{
    // =========================================================================
    // Accept min/max sleep constants (mirrors const.go ACCEPT_MIN_SLEEP / ACCEPT_MAX_SLEEP)
    // =========================================================================

    private static readonly TimeSpan AcceptMinSleep = TimeSpan.FromMilliseconds(10);
    private static readonly TimeSpan AcceptMaxSleep  = TimeSpan.FromSeconds(1);

    // =========================================================================
    // HTTP monitoring path constants (mirrors RootPath / VarzPath / ... in server.go)
    // =========================================================================

    /// <summary>HTTP monitoring path constants. Mirrors the Go <c>const</c> block in server.go.</summary>
    public static class MonitorPaths
    {
        public const string Root        = "/";
        public const string Varz        = "/varz";
        public const string Connz       = "/connz";
        public const string Routez      = "/routez";
        public const string Gatewayz    = "/gatewayz";
        public const string Leafz       = "/leafz";
        public const string Subsz       = "/subsz";
        public const string Stacksz     = "/stacksz";
        public const string Accountz    = "/accountz";
        public const string AccountStatz = "/accstatz";
        public const string Jsz         = "/jsz";
        public const string Healthz     = "/healthz";
        public const string IPQueuesz   = "/ipqueuesz";
        public const string Raftz       = "/raftz";
        public const string Expvarz     = "/debug/vars";
    }

    // =========================================================================
    // Nonce (session 10 stubs — full implementation in session 11 auth.go)
    // =========================================================================

    /// <summary>
    /// Returns true if this server requires clients to send a nonce for auth.
    /// Mirrors Go <c>Server.NonceRequired()</c>.
    /// Mirrors Go <c>Server.nonceRequired()</c>.
    /// </summary>
    private bool NonceRequired()
    {
        _mu.EnterReadLock();
        try
        {
            return NonceRequiredInternal();
        }
        finally
        {
            _mu.ExitReadLock();
        }
    }

    /// <summary>
    /// Returns true if this server requires clients to send a nonce for auth.
    /// Lock should be held by caller for strict Go parity.
    /// Mirrors Go <c>Server.nonceRequired()</c>.
    /// </summary>
    internal bool NonceRequiredInternal()
        => GetOpts().AlwaysEnableNonce || (_nkeys?.Count > 0) || _trustedKeys != null || _proxiesKeyPairs.Count > 0;

    /// <summary>
    /// Fills <paramref name="nonce"/> with random bytes.
    /// Stub — full implementation in session 11.
    /// Mirrors Go <c>Server.generateNonce()</c>.
    /// </summary>
    private void GenerateNonce(byte[] nonce) => RandomNumberGenerator.Fill(nonce);

    // =========================================================================
    // INFO JSON serialisation (feature 3124)
    // =========================================================================

    /// <summary>
    /// Serialises <paramref name="info"/> to a JSON INFO line (<c>INFO {...}\r\n</c>).
    /// Mirrors Go <c>generateInfoJSON()</c>.
    /// </summary>
    internal static byte[] GenerateInfoJson(ServerInfo info)
    {
        var json = JsonSerializer.Serialize(info);
        var line = $"INFO {json}\r\n";
        return System.Text.Encoding.UTF8.GetBytes(line);
    }

    // =========================================================================
    // copyInfo — deep copy INFO struct (feature 3071)
    // =========================================================================

    /// <summary>
    /// Deep-copies the server's INFO struct.
    /// Server lock must be held on entry.
    /// Mirrors Go <c>Server.copyInfo()</c>.
    /// </summary>
    internal ServerInfo CopyInfo()
    {
        // Shallow clone (copies all value-type and reference fields).
        // ServerInfo is a class, so we must clone the object rather than copy the reference.
        var info = _info.ShallowClone();

        // Deep-copy the slice fields that may be modified independently.
        if (info.ClientConnectUrls is { Length: > 0 })
            info.ClientConnectUrls = [.. info.ClientConnectUrls];

        if (info.WsConnectUrls is { Length: > 0 })
            info.WsConnectUrls = [.. info.WsConnectUrls];

        return info;
    }

    // =========================================================================
    // AcceptLoop — exported for easier testing (feature 3052)
    // =========================================================================

    /// <summary>
    /// Starts the main client accept loop. The optional <paramref name="ready"/>
    /// TCS is completed once the listener is bound (mirrors Go <c>chan struct{}</c>).
    /// Mirrors Go <c>Server.AcceptLoop()</c>.
    /// </summary>
    public void AcceptLoop(TaskCompletionSource? ready = null)
    {
        // If we were to exit before the listener is set up, signal the TCS.
        try
        {
            if (IsShuttingDown()) return;

            var opts = GetOpts();

            _mu.EnterWriteLock();
            var hp = $"{opts.Host}:{opts.Port}";
            var result = GetServerListener(hp);
            _listenerErr = result.err;

            if (result.err != null)
            {
                _mu.ExitWriteLock();
                Fatalf("Error listening on port: {0}, {1}", hp, result.err.Message);
                return;
            }

            var l = result.listener!;
            Noticef("Listening for client connections on {0}", l.LocalEndpoint);

            if (opts.TlsConfig != null)
            {
                Noticef("TLS required for client connections");
            }

            // If opts.Port was 0, write resolved port back.
            if (opts.Port == 0)
                opts.Port = ((IPEndPoint)l.LocalEndpoint!).Port;

            // Set the server's info Host/Port.
            var infoErr = SetInfoHostPort();
            if (infoErr != null)
            {
                Fatalf("Error setting server INFO with ClientAdvertise value of {0}, err={1}",
                    opts.ClientAdvertise, infoErr.Message);
                l.Stop();
                _mu.ExitWriteLock();
                return;
            }

            _clientConnectUrls.Clear();
            _clientConnectUrls.AddRange(GetClientConnectURLs());
            _listener = l;
            StartWebsocketServer();

            // Start the accept goroutine.
            _ = Task.Run(() =>
            {
                _reloadMu.EnterReadLock();
                AcceptConnections(l, "Client",
                    tc => CreateClient(tc),
                    _ =>
                    {
                        if (IsLameDuckMode())
                        {
                            // Signal that we are not accepting new clients.
                            _ldmCh?.Writer.TryWrite(default);
                            // Wait for quit signal.
                            _quitCts.Token.WaitHandle.WaitOne();
                            return true;
                        }
                        return false;
                    });
                _reloadMu.ExitReadLock();
            });

            _mu.ExitWriteLock();
        }
        finally
        {
            // Signal that we are ready (or exiting early).
            ready?.TrySetResult();
        }
    }

    // =========================================================================
    // GetServerListener (feature 3054)
    // =========================================================================

    /// <summary>
    /// Returns the existing client listener (if any) or creates a new TCP listener on <paramref name="hp"/>.
    /// Server lock must be held on entry.
    /// Mirrors Go <c>Server.getServerListener()</c>.
    /// </summary>
    private (TcpListener? listener, Exception? err) GetServerListener(string hp)
    {
        if (_listener != null)
            return (_listener, _listenerErr);

        try
        {
            var parts = hp.Split(':', 2);
            var host  = parts[0];
            var port  = parts.Length > 1 ? int.Parse(parts[1]) : 0;
            var addr  = string.IsNullOrEmpty(host) || host == "0.0.0.0"
                ? IPAddress.Any
                : (host == "::" ? IPAddress.IPv6Any : IPAddress.Parse(host));

            var l = new TcpListener(addr, port);
            l.Start();
            return (l, null);
        }
        catch (Exception ex)
        {
            return (null, ex);
        }
    }

    // =========================================================================
    // InProcessConn (feature 3055)
    // =========================================================================

    /// <summary>
    /// Returns an in-process connection to the server, bypassing the need for
    /// a TCP listener for local connectivity within the same process.
    /// Mirrors Go <c>Server.InProcessConn()</c>.
    /// </summary>
    public (Stream? conn, Exception? err) InProcessConn()
    {
        // Create a loopback TCP socket pair to simulate net.Pipe().
        TcpListener? loopback = null;
        TcpClient?   clientTcp = null;
        TcpClient?   serverTcp = null;

        try
        {
            loopback = new TcpListener(IPAddress.Loopback, 0);
            loopback.Start();
            var ep = (IPEndPoint)loopback.LocalEndpoint!;

            clientTcp = new TcpClient();
            clientTcp.Connect(ep);
            serverTcp = loopback.AcceptTcpClient();
        }
        catch (Exception ex)
        {
            clientTcp?.Dispose();
            serverTcp?.Dispose();
            return (null, ex);
        }
        finally
        {
            loopback?.Stop();
        }

        var serverStream = serverTcp.GetStream();
        var clientStream = clientTcp.GetStream();

        if (!StartGoRoutine(() => CreateClientInProcess(serverStream)))
        {
            serverStream.Dispose();
            clientStream.Dispose();
            return (null, new InvalidOperationException("failed to create connection"));
        }

        return (clientStream, null);
    }

    // =========================================================================
    // AcceptConnections — inner accept loop (feature 3056)
    // =========================================================================

    /// <summary>
    /// Runs the TCP accept loop for <paramref name="l"/>, calling <paramref name="createFunc"/>
    /// for each accepted connection. On error, calls <paramref name="errFunc"/> and backs off.
    /// Signals <c>_done</c> when the loop exits.
    /// Mirrors Go <c>Server.acceptConnections()</c>.
    /// </summary>
    private void AcceptConnections(
        TcpListener              l,
        string                   acceptName,
        Action<TcpClient>        createFunc,
        Func<Exception, bool>?   errFunc = null)
    {
        var tmpDelay = AcceptMinSleep;

        while (true)
        {
            TcpClient tc;
            try
            {
                tc = l.AcceptTcpClient();
            }
            catch (Exception err)
            {
                if (errFunc != null && errFunc(err))
                    break;

                var newDelay = AcceptError(acceptName, err, tmpDelay);
                if (newDelay < TimeSpan.Zero)
                    break;

                tmpDelay = newDelay;
                continue;
            }

            tmpDelay = AcceptMinSleep;

            var captured = tc;
            if (!StartGoRoutine(() =>
            {
                try
                {
                    _reloadMu.EnterReadLock();
                    createFunc(captured);
                    _reloadMu.ExitReadLock();
                }
                catch
                {
                    _reloadMu.ExitReadLock();
                    captured.Dispose();
                }
            }))
            {
                tc.Dispose();
            }
        }

        Debugf("{0} accept loop exiting..", acceptName);
        _done.Writer.TryWrite(default);
    }

    // =========================================================================
    // Profiler (feature 3057)
    // =========================================================================

    /// <summary>
    /// Starts the HTTP profiling server.
    /// Stub — full pprof integration deferred to session 17.
    /// Mirrors Go <c>Server.StartProfiler()</c>.
    /// </summary>
    public void StartProfiler()
    {
        if (IsShuttingDown()) return;

        var opts = GetOpts();
        var port = opts.ProfPort == -1 ? 0 : opts.ProfPort;

        _mu.EnterWriteLock();
        try
        {
            var hp = $"{opts.Host}:{port}";
            var parts = hp.Split(':', 2);
            var addr  = string.IsNullOrEmpty(parts[0]) ? IPAddress.Any : IPAddress.Parse(parts[0]);
            var l = new TcpListener(addr, port);
            l.Start();
            Noticef("profiling port: {0}", ((IPEndPoint)l.LocalEndpoint!).Port);
            _profiler = l;
        }
        catch (Exception ex)
        {
            Fatalf("error starting profiler: {0}", ex);
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    /// <summary>
    /// Sets the block profiling rate. Stub — mirrors Go <c>Server.setBlockProfileRate()</c>.
    /// </summary>
    private void SetBlockProfileRate(int rate)
    {
        if (rate > 0)
            Warnf("Block profiling is enabled (rate {0}), this may have a performance impact", rate);
        // runtime.SetBlockProfileRate — not applicable in .NET; no-op.
    }

    // =========================================================================
    // HTTP Monitoring (features 3058–3061)
    // =========================================================================

    /// <summary>
    /// Enables the HTTP monitoring port.
    /// Deprecated: use <see cref="StartMonitoring"/> instead.
    /// Mirrors Go <c>Server.StartHTTPMonitoring()</c>.
    /// </summary>
    public void StartHTTPMonitoring() => StartMonitoringInternal(false);

    /// <summary>
    /// Enables the HTTPS monitoring port.
    /// Deprecated: use <see cref="StartMonitoring"/> instead.
    /// Mirrors Go <c>Server.StartHTTPSMonitoring()</c>.
    /// </summary>
    public void StartHTTPSMonitoring() => StartMonitoringInternal(true);

    /// <summary>
    /// Starts the HTTP or HTTPS monitoring server as configured.
    /// Mirrors Go <c>Server.StartMonitoring()</c>.
    /// </summary>
    public Exception? StartMonitoring()
    {
        var opts = GetOpts();
        if (opts.HttpPort != 0 && opts.HttpsPort != 0)
            return new InvalidOperationException(
                $"can't specify both HTTP ({opts.HttpPort}) and HTTPs ({opts.HttpsPort}) ports");

        Exception? err = null;
        if (opts.HttpPort != 0)
            err = StartMonitoringInternal(false);
        else if (opts.HttpsPort != 0)
        {
            if (opts.TlsConfig == null)
                return new InvalidOperationException("TLS cert and key required for HTTPS");
            err = StartMonitoringInternal(true);
        }
        return err;
    }

    /// <summary>
    /// Returns the base URL path for the monitoring server.
    /// Mirrors Go <c>Server.basePath()</c>.
    /// </summary>
    private string BasePath(string p) =>
        string.IsNullOrEmpty(_httpBasePath) ? p : System.IO.Path.Combine(_httpBasePath, p.TrimStart('/'));

    /// <summary>
    /// Returns a monitoring TLS config cloned from server TLS config, with client certs disabled.
    /// Mirrors Go <c>Server.getMonitoringTLSConfig()</c>.
    /// </summary>
    internal SslServerAuthenticationOptions? GetMonitoringTLSConfig()
    {
        var opts = GetOpts();
        if (opts.TlsConfig == null)
            return null;

        var clone = new SslServerAuthenticationOptions
        {
            EnabledSslProtocols = opts.TlsConfig.EnabledSslProtocols,
            AllowRenegotiation = opts.TlsConfig.AllowRenegotiation,
            CertificateRevocationCheckMode = opts.TlsConfig.CertificateRevocationCheckMode,
            CipherSuitesPolicy = opts.TlsConfig.CipherSuitesPolicy,
            ServerCertificate = opts.TlsConfig.ServerCertificate,
            ClientCertificateRequired = false,
            EncryptionPolicy = opts.TlsConfig.EncryptionPolicy,
            CertificateChainPolicy = opts.TlsConfig.CertificateChainPolicy,
        };
        return clone;
    }

    /// <summary>
    /// Returns the monitoring handler object while monitoring is active, otherwise null.
    /// Mirrors Go <c>Server.HTTPHandler()</c>.
    /// </summary>
    public object? HTTPHandler()
    {
        _mu.EnterReadLock();
        try { return _httpHandler; }
        finally { _mu.ExitReadLock(); }
    }

    /// <summary>
    /// Starts the HTTP/HTTPS monitoring listener.
    /// Stub — full monitoring handler registration deferred to session 17.
    /// Mirrors Go <c>Server.startMonitoring()</c>.
    /// </summary>
    private Exception? StartMonitoringInternal(bool secure)
    {
        if (IsShuttingDown()) return null;

        var opts = GetOpts();
        int port;
        string proto;

        if (secure)
        {
            proto = "https";
            port  = opts.HttpsPort == -1 ? 0 : opts.HttpsPort;
        }
        else
        {
            proto = "http";
            port  = opts.HttpPort == -1 ? 0 : opts.HttpPort;
        }

        var hp = $"{opts.HttpHost}:{port}";
        TcpListener httpListener;
        try
        {
            var parts = hp.Split(':', 2);
            var bindAddr = string.IsNullOrEmpty(parts[0]) ? IPAddress.Any : IPAddress.Parse(parts[0]);
            httpListener = new TcpListener(bindAddr, port);
            httpListener.Start();
        }
        catch (Exception ex)
        {
            return new InvalidOperationException($"can't listen to the monitor port: {ex.Message}", ex);
        }

        var rport = ((IPEndPoint)httpListener.LocalEndpoint!).Port;
        Noticef("Starting {0} monitor on {1}", proto, $"{opts.HttpHost}:{rport}");

        _mu.EnterWriteLock();
        _http = httpListener;
        _httpHandler = new object();
        _mu.ExitWriteLock();

        // Full HTTP handler registration in session 17; for now just drain the listener.
        // Use a long-running task so teardown does not depend on thread-pool availability.
        _ = Task.Factory.StartNew(() =>
        {
            // Accept and immediately close connections until shutdown.
            while (!IsShuttingDown())
            {
                try
                {
                    var tc = httpListener.AcceptTcpClient();
                    tc.Dispose();
                }
                catch
                {
                    break;
                }
            }

            _mu.EnterWriteLock();
            _httpHandler = null;
            _mu.ExitWriteLock();

            _done.Writer.TryWrite(default);
        }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);

        return null;
    }

    // =========================================================================
    // CreateClient / CreateClientInProcess / CreateClientEx (features 3063–3065)
    // =========================================================================

    /// <summary>
    /// Creates and registers a new client connection from the given TCP client.
    /// Mirrors Go <c>Server.createClient()</c>.
    /// </summary>
    private ClientConnection? CreateClient(TcpClient tc) =>
        CreateClientEx(tc.GetStream(), false);

    /// <summary>
    /// Creates and registers an in-process client connection.
    /// Mirrors Go <c>Server.createClientInProcess()</c>.
    /// </summary>
    private ClientConnection? CreateClientInProcess(Stream nc) =>
        CreateClientEx(nc, true);

    /// <summary>
    /// Full client-creation factory: registers with account, sends INFO, starts read/write loops.
    /// Mirrors Go <c>Server.createClientEx()</c>.
    /// </summary>
    private ClientConnection? CreateClientEx(Stream nc, bool inProcess)
    {
        var opts = GetOpts();
        var now  = DateTime.UtcNow;

        var c = new ClientConnection(ClientKind.Client, this, nc)
        {
            InProc = inProcess,
            Start  = now,
            Last   = now,
            Opts   = ClientOptions.Default,
        };

        var globalAcc = GlobalAccount();
        if (globalAcc != null)
        {
            try { c.RegisterWithAccount(globalAcc); }
            catch (Exception ex)
            {
                c.ReportErrRegisterAccount(globalAcc, ex);
                return null;
            }
        }

        ServerInfo info;
        bool       authRequired;

        _mu.EnterWriteLock();
        info = CopyInfo();

        if (NonceRequired())
        {
            var raw = new byte[24]; // nonceLen
            GenerateNonce(raw);
            info.Nonce = System.Text.Encoding.ASCII.GetString(raw);
        }

        c.Nonce       = info.Nonce != null ? System.Text.Encoding.ASCII.GetBytes(info.Nonce) : null;
        authRequired  = info.AuthRequired;

        // If auth is required but there is a no_auth_user, don't require it.
        if (info.AuthRequired
            && !string.IsNullOrEmpty(opts.NoAuthUser)
            && opts.NoAuthUser != _sysAccOnlyNoAuthUser)
        {
            info.AuthRequired = false;
        }

        // For in-process connections with TLS required, mark as available but not required.
        if (inProcess && info.TlsRequired)
        {
            info.TlsRequired  = false;
            info.TlsAvailable = true;
        }

        _totalClients++;
        _mu.ExitWriteLock();

        lock (c)
        {
            if (authRequired)
                c.Flags |= ClientFlags.ExpectConnect;

            c.InitClient();
            c.Debugf("Client connection created");

            // Send INFO to the client immediately (TLS-first handling omitted — stub).
            var infoBytes = c.GenerateClientInfoJSON(info, !opts.ProxyProtocol);
            c.SendProtoNow(infoBytes.Span);
        }

        // Register with the server.
        _mu.EnterWriteLock();
        if (!IsRunning() || _ldm)
        {
            if (IsShuttingDown())
                nc.Close();
            _mu.ExitWriteLock();
            return c;
        }

        if (opts.MaxConn > 0 && _clients.Count >= opts.MaxConn)
        {
            _mu.ExitWriteLock();
            c.MaxConnExceeded();
            return null;
        }

        _clients[c.Cid] = c;
        _mu.ExitWriteLock();

        lock (c)
        {
            if (c.IsClosed())
            {
                c.CloseConnection(ClosedState.WriteError);
                return null;
            }

            if (authRequired)
                c.SetAuthTimer(TimeSpan.FromSeconds(opts.AuthTimeout));

            c.SetPingTimer();

            // Start read/write loops as goroutines.
            byte[]? pre = null;
            StartGoRoutine(() => c.ReadLoop(pre));
            StartGoRoutine(() => c.WriteLoop());
        }

        return c;
    }

    // =========================================================================
    // SaveClosedClient (feature 3066)
    // =========================================================================

    /// <summary>
    /// Records a closed client in the ring buffer for the /connz monitoring endpoint.
    /// Mirrors Go <c>Server.saveClosedClient()</c>.
    /// </summary>
    internal void SaveClosedClient(ClientConnection c, Stream? nc, ClosedState reason)
    {
        var now = DateTime.UtcNow;

        // Stub: full implementation requires connInfo.Fill() from session 17.
        // For now create a minimal ClosedClient record.
        AccountDisconnectEvent(c, now, reason.ToString());

        string user = string.Empty;
        string acc  = string.Empty;

        lock (c)
        {
            // acc name if not the global account.
            var account = c.GetAccount();
            if (account?.Name != null && account.Name != ServerConstants.DefaultGlobalAccount)
                acc = account.Name;
        }

        var cc = new ClosedClient
        {
            User    = user,
            Account = acc,
        };

        _mu.EnterWriteLock();
        _closed.Append(cc);
        _mu.ExitWriteLock();
    }

    // =========================================================================
    // Connect URL management (features 3067–3070)
    // =========================================================================

    /// <summary>
    /// Adds URLs to the connect-URL maps and sends async INFO to clients.
    /// Server lock must be held on entry.
    /// Mirrors Go <c>Server.addConnectURLsAndSendINFOToClients()</c>.
    /// </summary>
    internal void AddConnectURLsAndSendINFOToClients(string[] curls, string[] wsurls) =>
        UpdateServerINFOAndSendINFOToClients(curls, wsurls, add: true);

    /// <summary>
    /// Removes URLs from the connect-URL maps and sends async INFO to clients.
    /// Server lock must be held on entry.
    /// Mirrors Go <c>Server.removeConnectURLsAndSendINFOToClients()</c>.
    /// </summary>
    internal void RemoveConnectURLsAndSendINFOToClients(string[] curls, string[] wsurls) =>
        UpdateServerINFOAndSendINFOToClients(curls, wsurls, add: false);

    /// <summary>
    /// Updates the connect-URL maps and, if changed, sends async INFO to clients.
    /// Server lock must be held on entry.
    /// Mirrors Go <c>Server.updateServerINFOAndSendINFOToClients()</c>.
    /// </summary>
    private void UpdateServerINFOAndSendINFOToClients(string[] curls, string[] wsurls, bool add)
    {
        bool UpdateMap(string[] urls, RefCountedUrlSet m)
        {
            bool wasUpdated = false;
            foreach (var url in urls)
            {
                if (add  && m.AddUrl(url))    wasUpdated = true;
                if (!add && m.RemoveUrl(url)) wasUpdated = true;
            }
            return wasUpdated;
        }

        bool cliUpdated = UpdateMap(curls, _clientConnectUrlsMap);
        bool wsUpdated  = UpdateMap(wsurls, _websocket.ConnectUrlsMap);

        void UpdateInfo(ref string[]? infoUrls, List<string> localUrls, RefCountedUrlSet m)
        {
            var list = new List<string>(localUrls);
            list.AddRange(m.GetAsStringSlice());
            infoUrls = list.Count > 0 ? list.ToArray() : null;
        }

        if (cliUpdated)
        {
            var infoUrls = _info.ClientConnectUrls;
            UpdateInfo(ref infoUrls, _clientConnectUrls, _clientConnectUrlsMap);
            _info.ClientConnectUrls = infoUrls;
        }

        if (wsUpdated)
        {
            var wsUrls = _websocket.ConnectUrlsMap.GetAsStringSlice();
            _info.WsConnectUrls = wsUrls.Length > 0 ? wsUrls : null;
        }

        if (cliUpdated || wsUpdated)
            SendAsyncInfoToClients(cliUpdated, wsUpdated);
    }

    // =========================================================================
    // TLS version helpers (features 3072–3073)
    // =========================================================================

    /// <summary>
    /// Returns the TLS version string for the given TLS version number.
    /// Mirrors Go <c>tlsVersion()</c>.
    /// </summary>
    internal static string TlsVersion(ushort ver) => ver switch
    {
        0x0301 => "1.0",
        0x0302 => "1.1",
        0x0303 => "1.2",
        0x0304 => "1.3",
        _      => $"Unknown [0x{ver:x}]",
    };

    /// <summary>
    /// Parses a TLS version string to a version number.
    /// Mirrors Go <c>tlsVersionFromString()</c>.
    /// </summary>
    internal static (ushort ver, Exception? err) TlsVersionFromString(string ver) => ver switch
    {
        "1.0" => (0x0301, null),
        "1.1" => (0x0302, null),
        "1.2" => (0x0303, null),
        "1.3" => (0x0304, null),
        _     => (0, new ArgumentException($"unknown version: {ver}")),
    };

    /// <summary>
    /// Handles TLS handshake timeout by closing the connection if auth is incomplete.
    /// Mirrors Go <c>tlsTimeout</c>.
    /// </summary>
    internal static void TlsTimeout(ClientConnection c, SslStream conn)
    {
        lock (c)
        {
            if (c.IsClosed())
                return;
        }

        if (!conn.IsAuthenticated)
        {
            c.Errorf("TLS handshake timeout");
            c.SendErr("Secure Connection - TLS Required");
            c.CloseConnection(ClosedState.TlsHandshakeError);
        }
    }

    // =========================================================================
    // Connect URL helpers (features 3074–3076)
    // =========================================================================

    /// <summary>
    /// Returns client connect URLs based on server options.
    /// Server lock must be held on entry.
    /// Mirrors Go <c>Server.getClientConnectURLs()</c>.
    /// </summary>
    private List<string> GetClientConnectURLs()
    {
        var opts = GetOpts();
        var (urls, _) = GetConnectURLs(opts.ClientAdvertise, opts.Host, opts.Port);
        return urls;
    }

    /// <summary>
    /// Returns connect URLs for the given advertise/host/port.
    /// Mirrors Go <c>Server.getConnectURLs()</c>.
    /// </summary>
    private (List<string> urls, Exception? err) GetConnectURLs(string advertise, string host, int port)
    {
        var urls = new List<string>();

        if (!string.IsNullOrEmpty(advertise))
        {
            var (h, p, err) = ServerUtilities.ParseHostPort(advertise, port);
            if (err != null) return (urls, err);
            urls.Add($"{h}:{p}");
        }
        else
        {
            var sPort = port.ToString();
            var (isAny, ips, _) = GetNonLocalIPsIfHostIsIPAny(host, all: true);
            foreach (var ip in ips)
                urls.Add($"{ip}:{sPort}");

            if (!isAny || urls.Count == 0)
            {
                if (host != "0.0.0.0" && host != "::")
                    urls.Add($"{host}:{sPort}");
                else
                    Errorf("Address {0} can not be resolved properly", host);
            }
        }

        return (urls, null);
    }

    /// <summary>
    /// Returns the non-local IPs if <paramref name="host"/> is 0.0.0.0 or ::.
    /// Returns (isAny, ips, err).
    /// Mirrors Go <c>Server.getNonLocalIPsIfHostIsIPAny()</c>.
    /// </summary>
    private (bool isAny, List<string> ips, Exception? err) GetNonLocalIPsIfHostIsIPAny(string host, bool all)
    {
        if (!IPAddress.TryParse(host, out var ip))
            return (false, [], null);

        if (!ip.Equals(IPAddress.Any) && !ip.Equals(IPAddress.IPv6Any))
            return (false, [], null);

        Debugf("Get non local IPs for \"{0}\"", host);

        var ips = new List<string>();
        foreach (var iface in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
        {
            foreach (var addr in iface.GetIPProperties().UnicastAddresses)
            {
                var a = addr.Address;
                if (!a.IsIPv6LinkLocal && !a.Equals(IPAddress.Loopback) &&
                    !a.Equals(IPAddress.IPv6Loopback) &&
                    a.AddressFamily is System.Net.Sockets.AddressFamily.InterNetwork
                        or System.Net.Sockets.AddressFamily.InterNetworkV6)
                {
                    Debugf("  ip={0}", a);
                    ips.Add(a.ToString());
                    if (!all) return (true, ips, null);
                }
            }
        }

        return (true, ips, null);
    }

    // =========================================================================
    // Port helpers (features 3077–3080)
    // =========================================================================

    /// <summary>
    /// Resolves host-port strings from a TcpListener, expanding unspecified addresses.
    /// Mirrors Go <c>resolveHostPorts()</c>.
    /// </summary>
    private static List<string> ResolveHostPorts(TcpListener l)
    {
        var result = new List<string>();
        if (l.LocalEndpoint is not IPEndPoint ep) return result;

        var port = ep.Port.ToString();

        if (ep.Address.Equals(IPAddress.Any) || ep.Address.Equals(IPAddress.IPv6Any))
        {
            foreach (var iface in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
                foreach (var addr in iface.GetIPProperties().UnicastAddresses)
                    result.Add($"{addr.Address}:{port}");
        }
        else
        {
            result.Add($"{ep.Address}:{port}");
        }

        return result;
    }

    /// <summary>
    /// Formats a listener's addresses as protocol URLs.
    /// Mirrors Go <c>formatURL()</c>.
    /// </summary>
    private static string[] FormatUrl(string protocol, TcpListener l)
    {
        var hostPorts = ResolveHostPorts(l);
        for (int i = 0; i < hostPorts.Count; i++)
            hostPorts[i] = $"{protocol}://{hostPorts[i]}";
        return hostPorts.ToArray();
    }

    /// <summary>
    /// Returns port info once all expected listeners are up, or null if timeout.
    /// Mirrors Go <c>Server.PortsInfo()</c>.
    /// </summary>
    public Ports? PortsInfo(TimeSpan maxWait)
    {
        if (!ReadyForListeners(maxWait)) return null;

        var opts = GetOpts();

        _mu.EnterReadLock();
        bool     tlsRequired      = _info.TlsRequired;
        var      listener         = _listener;
        var      httpListener     = _http;
        var      clusterListener  = _routeListener;
        var      profileListener  = _profiler;
        _mu.ExitReadLock();

        var ports = new Ports();

        if (listener != null)
        {
            var natsProto = tlsRequired ? "tls" : "nats";
            ports.Nats = FormatUrl(natsProto, listener);
        }

        if (httpListener != null)
        {
            var monProto = opts.HttpsPort != 0 ? "https" : "http";
            ports.Monitoring = FormatUrl(monProto, httpListener);
        }

        if (clusterListener != null)
        {
            var clusterProto = opts.Cluster.TlsConfig != null ? "tls" : "nats";
            ports.Cluster = FormatUrl(clusterProto, clusterListener);
        }

        if (profileListener != null)
            ports.Profile = FormatUrl("http", profileListener);

        return ports;
    }

    /// <summary>
    /// Returns the path of the ports file.
    /// Mirrors Go <c>Server.portFile()</c>.
    /// </summary>
    private string PortFile(string dirHint)
    {
        var dirname = GetOpts().PortsFileDir;
        if (!string.IsNullOrEmpty(dirHint)) dirname = dirHint;
        if (string.IsNullOrEmpty(dirname))  return string.Empty;

        var exe = System.Diagnostics.Process.GetCurrentProcess().ProcessName;
        var pid = System.Diagnostics.Process.GetCurrentProcess().Id;
        return System.IO.Path.Combine(dirname, $"{exe}_{pid}.ports");
    }

    /// <summary>
    /// Deletes the ports file.
    /// Mirrors Go <c>Server.deletePortsFile()</c>.
    /// </summary>
    private void DeletePortsFile(string hintDir)
    {
        var file = PortFile(hintDir);
        if (string.IsNullOrEmpty(file)) return;
        try { System.IO.File.Delete(file); }
        catch (Exception ex) { Errorf("Error cleaning up ports file {0}: {1}", file, ex.Message); }
    }

    /// <summary>
    /// Writes a Ports JSON file if a ports-file directory is configured.
    /// Mirrors Go <c>Server.logPorts()</c>.
    /// </summary>
    private void LogPorts()
    {
        var opts      = GetOpts();
        var portsFile = PortFile(opts.PortsFileDir);
        if (string.IsNullOrEmpty(portsFile)) return;

        _ = Task.Run(() =>
        {
            var info = PortsInfo(TimeSpan.FromSeconds(5));
            if (info == null)
            {
                Errorf("Unable to resolve the ports in the specified time");
                return;
            }

            try
            {
                var data = JsonSerializer.SerializeToUtf8Bytes(info);
                System.IO.File.WriteAllBytes(portsFile, data);
            }
            catch (Exception ex)
            {
                Errorf("Error writing ports file ({0}): {1}", portsFile, ex.Message);
            }
        });
    }

    // =========================================================================
    // readyForListeners / serviceListeners (features 3127–3128)
    // =========================================================================

    /// <summary>
    /// Waits until all expected listeners are resolved or <paramref name="dur"/> expires.
    /// Mirrors Go <c>Server.readyForListeners()</c>.
    /// </summary>
    private bool ReadyForListeners(TimeSpan dur)
    {
        var end = DateTime.UtcNow.Add(dur);
        while (DateTime.UtcNow < end)
        {
            _mu.EnterReadLock();
            var listeners = ServiceListeners();
            _mu.ExitReadLock();

            if (listeners.Length == 0) return false;

            bool ok = true;
            foreach (var l in listeners)
            {
                if (l == null) { ok = false; break; }
            }

            if (ok) return true;

            try
            {
                Task.Delay(TimeSpan.FromMilliseconds(25), _quitCts.Token)
                    .GetAwaiter().GetResult();
            }
            catch (OperationCanceledException)
            {
                return false;
            }
        }

        return false;
    }

    /// <summary>
    /// Returns the list of expected listeners (may include nulls for not-yet-bound listeners).
    /// Server lock must be held on entry.
    /// Mirrors Go <c>Server.serviceListeners()</c>.
    /// </summary>
    private TcpListener?[] ServiceListeners()
    {
        var opts  = GetOpts();
        var list  = new List<TcpListener?> { _listener };

        if (opts.Cluster.Port != 0)          list.Add(_routeListener);
        if (opts.HttpPort != 0 || opts.HttpsPort != 0) list.Add(_http);
        if (opts.ProfPort != 0)              list.Add(_profiler);
        if (opts.Websocket.Port != 0)       list.Add(_websocket.Listener);

        return list.ToArray();
    }

    // =========================================================================
    // acceptError (feature 3129)
    // =========================================================================

    /// <summary>
    /// Handles a transient accept error by sleeping and doubling the delay.
    /// Returns a negative TimeSpan if the server has stopped.
    /// Mirrors Go <c>Server.acceptError()</c>.
    /// </summary>
    private TimeSpan AcceptError(string acceptName, Exception err, TimeSpan tmpDelay)
    {
        if (!IsRunning()) return TimeSpan.FromSeconds(-1);

        if (err is SocketException se && se.SocketErrorCode == SocketError.WouldBlock)
        {
            Errorf("Temporary {0} Accept Error({1}), sleeping {2}ms",
                acceptName, err.Message, (int)tmpDelay.TotalMilliseconds);

            try
            {
                Task.Delay(tmpDelay, _quitCts.Token).GetAwaiter().GetResult();
            }
            catch (OperationCanceledException)
            {
                return TimeSpan.FromSeconds(-1);
            }

            tmpDelay *= 2;
            if (tmpDelay > AcceptMaxSleep) tmpDelay = AcceptMaxSleep;
        }
        else
        {
            Errorf("{0} Accept error: {1}", acceptName, err.Message);
        }

        return tmpDelay;
    }

    // =========================================================================
    // GetRandomIP (feature 3130)
    // =========================================================================

    private static readonly Exception ErrNoIPAvail = new InvalidOperationException("no IP available");

    /// <summary>
    /// Resolves a hostname to a random IP address, excluding addresses in <paramref name="excluded"/>.
    /// Mirrors Go <c>Server.getRandomIP()</c>.
    /// </summary>
    internal async Task<(string address, Exception? err)> GetRandomIP(
        INetResolver          resolver,
        string                url,
        HashSet<string>?      excluded = null)
    {
        var colonIdx = url.LastIndexOf(':');
        if (colonIdx < 0) return (url, new FormatException($"invalid host:port: {url}"));

        var host = url[..colonIdx];
        var port = url[(colonIdx + 1)..];

        // If already an IP address, return as-is.
        if (IPAddress.TryParse(host, out _)) return (url, null);

        string[] ips;
        try
        {
            ips = await resolver.LookupHostAsync(host, _quitCts.Token);
        }
        catch (Exception ex)
        {
            return (string.Empty, new InvalidOperationException($"lookup for host \"{host}\": {ex.Message}", ex));
        }

        if (excluded != null && excluded.Count > 0)
        {
            var filtered = new List<string>(ips.Length);
            foreach (var ip in ips)
            {
                var addr = $"{ip}:{port}";
                if (!excluded.Contains(addr))
                    filtered.Add(ip);
            }

            ips = filtered.ToArray();
            if (ips.Length == 0) return (string.Empty, ErrNoIPAvail);
        }

        if (ips.Length == 0)
        {
            Warnf("Unable to get IP for {0}, will try with {1}", host, url);
            return (url, null);
        }

        var chosen = ips.Length == 1 ? ips[0] : ips[Random.Shared.Next(ips.Length)];
        return ($"{chosen}:{port}", null);
    }
}
