// Copyright 2013-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/monitor.go in the NATS server Go source.

using System.Diagnostics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

// ============================================================================
// Session 17: Monitor Endpoints
// Mirrors server/monitor.go in the NATS server Go source.
// ============================================================================

public sealed partial class NatsServer
{
    // =========================================================================
    // Uptime / duration helpers
    // =========================================================================

    /// <summary>
    /// Formats a <see cref="TimeSpan"/> as "XdXhXmXs".
    /// Mirrors Go <c>myUptime(d time.Duration) string</c> in monitor.go.
    /// </summary>
    internal static string MyUptime(TimeSpan d)
    {
        var days    = (int)d.TotalDays;
        var hours   = d.Hours;
        var minutes = d.Minutes;
        var seconds = d.Seconds;
        return $"{days}d{hours}h{minutes}m{seconds}s";
    }

    // =========================================================================
    // HTTP response helpers
    // =========================================================================

    private static readonly JsonSerializerOptions MonitorJsonOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        WriteIndented          = false,
    };

    /// <summary>
    /// Writes a JSON body as the HTTP response.
    /// Mirrors Go <c>ResponseHandler(w http.ResponseWriter, r *http.Request, data []byte)</c>.
    /// </summary>
    internal static void ResponseHandler(
        System.Net.HttpListenerRequest  request,
        System.Net.HttpListenerResponse response,
        byte[]                          data)
    {
        // CORS
        response.Headers["Access-Control-Allow-Origin"] = "*";

        // JSONP support
        var callback = request.QueryString["callback"] ?? string.Empty;
        if (!string.IsNullOrEmpty(callback))
        {
            response.ContentType = "application/javascript";
            var js = Encoding.UTF8.GetBytes($"{callback}({Encoding.UTF8.GetString(data)})");
            response.ContentLength64 = js.Length;
            response.OutputStream.Write(js);
            response.OutputStream.Close();
            return;
        }

        response.ContentType = "application/json";
        response.StatusCode  = 200;
        response.ContentLength64 = data.Length;
        response.OutputStream.Write(data);
        response.OutputStream.Close();
    }

    /// <summary>
    /// Writes an error response with the given HTTP status code.
    /// Mirrors Go <c>handleResponse(code int, w http.ResponseWriter, r *http.Request, data []byte)</c>.
    /// </summary>
    private static void HandleResponse(
        int                             code,
        System.Net.HttpListenerRequest  request,
        System.Net.HttpListenerResponse response,
        byte[]                          data)
    {
        response.Headers["Access-Control-Allow-Origin"] = "*";
        response.ContentType  = "application/json";
        response.StatusCode   = code;
        response.ContentLength64 = data.Length;
        response.OutputStream.Write(data);
        response.OutputStream.Close();
    }

    // =========================================================================
    // Query parameter decode helpers
    // =========================================================================

    private static bool DecodeBool(System.Collections.Specialized.NameValueCollection q, string key, bool defaultVal = false)
    {
        var v = q[key];
        if (v is null) return defaultVal;
        return v is "1" or "true" or "yes";
    }

    private static int DecodeInt(System.Collections.Specialized.NameValueCollection q, string key, int defaultVal = 0)
    {
        var v = q[key];
        if (v is null) return defaultVal;
        return int.TryParse(v, out var n) ? n : defaultVal;
    }

    private static ulong DecodeUint64(System.Collections.Specialized.NameValueCollection q, string key, ulong defaultVal = 0)
    {
        var v = q[key];
        if (v is null) return defaultVal;
        return ulong.TryParse(v, out var n) ? n : defaultVal;
    }

    private static ConnState DecodeState(System.Collections.Specialized.NameValueCollection q, ConnState defaultVal = ConnState.ConnOpen)
    {
        var v = q["state"];
        if (v is null) return defaultVal;
        return v.ToLowerInvariant() switch
        {
            "open"   => ConnState.ConnOpen,
            "closed" => ConnState.ConnClosed,
            "all"    => ConnState.ConnAll,
            _        => defaultVal,
        };
    }

    // =========================================================================
    // Connz — /connz
    // =========================================================================

    /// <summary>
    /// Returns a connection list snapshot.
    /// Mirrors Go <c>Server.Connz(opts *ConnzOptions) (*Connz, error)</c>.
    /// </summary>
    public (Connz Result, Exception? Error) Connz(ConnzOptions? opts = null)
    {
        opts ??= new ConnzOptions();

        if (opts.Limit == 0) opts.Limit = MonitorDefaults.DefaultConnListSize;

        var now = DateTime.UtcNow;

        // Collect connections.
        List<ConnInfo> conns = [];

        _mu.EnterReadLock();
        try
        {
            if (opts.State is ConnState.ConnOpen or ConnState.ConnAll)
            {
                foreach (var c in _clients.Values)
                {
                    var ci = FillConnInfo(c, opts, now);
                    if (!FilterConn(ci, opts)) continue;
                    conns.Add(ci);
                }
            }

            if (opts.State is ConnState.ConnClosed or ConnState.ConnAll)
            {
                // Closed connections stored in the ring buffer.
                var closed = _closed.ClosedClients();
                foreach (var cc in closed)
                {
                    if (cc is null) continue;
                    // Build a minimal ConnInfo from ClosedClient fields.
                    var ci = new ConnInfo
                    {
                        Start       = now, // no start time stored for closed connections
                        LastActivity = now,
                        Stop        = now, // approximate
                        Account     = cc.Account.Length > 0 ? cc.Account : null,
                        AuthorizedUser = cc.User.Length > 0 ? cc.User : null,
                    };
                    conns.Add(ci);
                }
            }
        }
        finally
        {
            _mu.ExitReadLock();
        }

        // Sort.
        var sortable = new ConnInfos(conns);
        SortConnInfos(sortable, opts.Sort);

        // Pagination.
        var total  = conns.Count;
        var offset = opts.Offset;
        if (offset < 0) offset = 0;
        if (offset > total) offset = total;

        var limit = opts.Limit;
        var end   = Math.Min(offset + limit, total);
        var page  = conns.GetRange(offset, end - offset);

        return (new Connz
        {
            Id      = ID(),
            Now     = now,
            NumConns = page.Count,
            Total   = total,
            Offset  = offset,
            Limit   = limit,
            Conns   = page,
        }, null);
    }

    /// <summary>
    /// Fills a <see cref="ConnInfo"/> from a live <see cref="ClientConnection"/>.
    /// Mirrors Go <c>ConnInfo.fill()</c>.
    /// </summary>
    private ConnInfo FillConnInfo(ClientConnection c, ConnzOptions opts, DateTime now)
    {
        ConnInfo ci;
        lock (c)
        {
            ci = new ConnInfo
            {
                Cid          = c.Cid,
                Kind         = c.KindString(),
                Type         = c.ClientTypeString(),
                Ip           = c.Host,
                Port         = c.Port,
                Start        = c.Start,
                LastActivity = c.Last,
                Uptime       = MyUptime(now - c.Start),
                Idle         = MyUptime(now - c.Last),
                Pending      = (int)c.OutPb,
                // InMsgs/OutMsgs/InBytes/OutBytes: not yet stored on ClientConnection
                InMsgs       = 0,
                OutMsgs      = 0,
                InBytes      = 0,
                OutBytes     = 0,
                Stalls       = 0,
                NumSubs      = (uint)c.Subs.Count,
                Name         = c.Opts.Name.Length > 0 ? c.Opts.Name : null,
                Lang         = c.Opts.Lang.Length > 0 ? c.Opts.Lang : null,
                Version      = c.Opts.Version.Length > 0 ? c.Opts.Version : null,
                NameTag      = c.NameTag.Length > 0 ? c.NameTag : null,
            };

            // Auth user
            if (opts.Username)
                ci.AuthorizedUser = c.GetRawAuthUser();

            // Account
            if (c._account is { } acc)
                ci.Account = acc.Name;

            // RTT
            if (c.Rtt > TimeSpan.Zero)
            {
                ci.RttNanos = (long)(c.Rtt.TotalMilliseconds * 1_000_000);
                ci.Rtt      = FormatRtt(c.Rtt);
            }

            // TLS
            if (c.GetTlsStream() is { } ssl)
            {
                var state = ssl.SslProtocol;
                ci.TlsVersion = state.ToString();
            }

            // Proxy
            if (c.ProxyKey.Length > 0)
                ci.Proxy = new ProxyInfo { Key = c.ProxyKey };

            // Subscriptions
            if (opts.SubscriptionsDetail)
            {
                ci.SubsDetail = [.. c.Subs.Select(kvp => new SubDetail
                {
                    Subject = kvp.Key,
                    Queue   = kvp.Value.Queue is { } q ? Encoding.UTF8.GetString(q) : null,
                    Sid     = kvp.Value.Sid is { } sid ? Encoding.UTF8.GetString(sid) : string.Empty,
                    Cid     = c.Cid,
                })];
            }
            else if (opts.Subscriptions)
            {
                ci.Subs = [.. c.Subs.Keys];
            }
        }

        return ci;
    }

    private static bool FilterConn(ConnInfo ci, ConnzOptions opts)
    {
        if (opts.Cid != 0 && ci.Cid != opts.Cid) return false;
        if (!string.IsNullOrEmpty(opts.User)    && ci.AuthorizedUser != opts.User) return false;
        if (!string.IsNullOrEmpty(opts.Account) && ci.Account != opts.Account) return false;
        return true;
    }

    /// <summary>
    /// Handles the /connz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleConnz()</c>.
    /// </summary>
    public void HandleConnz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Connz);
        var q = request.QueryString;
        var opts = new ConnzOptions
        {
            Sort               = (SortOpt)(q["sort"] ?? string.Empty),
            Username           = DecodeBool(q, "auth"),
            Subscriptions      = DecodeBool(q, "subs"),
            SubscriptionsDetail = DecodeBool(q, "subs") && DecodeBool(q, "detail"),
            Offset             = DecodeInt(q, "offset"),
            Limit              = DecodeInt(q, "limit", MonitorDefaults.DefaultConnListSize),
            Cid                = DecodeUint64(q, "cid"),
            State              = DecodeState(q),
            User               = q["user"] ?? string.Empty,
            Account            = q["acc"] ?? string.Empty,
            FilterSubject      = q["filter_subject"] ?? string.Empty,
        };

        var (result, err) = Connz(opts);
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(500, request, response, errBytes);
            return;
        }

        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Routez — /routez
    // =========================================================================

    /// <summary>
    /// Returns a route list snapshot.
    /// Mirrors Go <c>Server.Routez(opts *RoutezOptions) (*Routez, error)</c>.
    /// </summary>
    public Routez Routez(RoutezOptions? opts = null)
    {
        opts ??= new RoutezOptions();
        var now    = DateTime.UtcNow;
        var routes = new List<RouteMonitorInfo>();

        _mu.EnterReadLock();
        try
        {
            ForEachRoute(c =>
            {
                lock (c)
                {
                    var ri = FillRouteInfo(c, opts, now);
                    routes.Add(ri);
                }
            });
        }
        finally
        {
            _mu.ExitReadLock();
        }

        return new Routez
        {
            Id        = ID(),
            Name      = _info.Name,
            Now       = now,
            NumRoutes = routes.Count,
            Routes    = routes,
        };
    }

    private RouteMonitorInfo FillRouteInfo(ClientConnection c, RoutezOptions opts, DateTime now)
    {
        var ri = new RouteMonitorInfo
        {
            Rid          = c.Cid,
            Ip           = c.Host,
            Port         = c.Port,
            Start        = c.Start,
            LastActivity = c.Last,
            Uptime       = MyUptime(now - c.Start),
            Idle         = MyUptime(now - c.Last),
            Pending      = (int)c.OutPb,
            NumSubs      = (uint)c.Subs.Count,
        };

        if (c.Route is { } route)
        {
            ri.RemoteId     = route.RemoteId.Length > 0 ? route.RemoteId : null;
            ri.RemoteName   = route.RemoteName.Length > 0 ? route.RemoteName : null;
            ri.DidSolicit   = route.DidSolicit;
            ri.IsConfigured = route.RouteType == RouteType.Explicit;
            ri.Compression  = route.Compression.Length > 0 ? route.Compression : null;
        }

        // RTT
        if (c.Rtt > TimeSpan.Zero)
            ri.Rtt = FormatRtt(c.Rtt);

        // Account (pinned route)
        if (c.Route?.AccName is { } acc)
            ri.Account = Encoding.UTF8.GetString(acc);

        // Subscriptions
        if (opts.SubscriptionsDetail)
        {
            ri.SubsDetail = [.. c.Subs.Select(kvp => new SubDetail
            {
                Subject = kvp.Key,
                Queue   = kvp.Value.Queue is { } q ? Encoding.UTF8.GetString(q) : null,
                Sid     = kvp.Value.Sid is { } sid ? Encoding.UTF8.GetString(sid) : string.Empty,
                Cid     = c.Cid,
            })];
        }
        else if (opts.Subscriptions)
        {
            ri.Subs = [.. c.Subs.Keys];
        }

        return ri;
    }

    /// <summary>
    /// Handles the /routez HTTP endpoint.
    /// Mirrors Go <c>Server.HandleRoutez()</c>.
    /// </summary>
    public void HandleRoutez(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Routez);
        var q    = request.QueryString;
        var opts = new RoutezOptions
        {
            Subscriptions       = DecodeBool(q, "subs"),
            SubscriptionsDetail = DecodeBool(q, "subs") && DecodeBool(q, "detail"),
        };
        var result = Routez(opts);
        var data   = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Subsz — /subsz
    // =========================================================================

    /// <summary>
    /// Returns subscription stats snapshot.
    /// Mirrors Go <c>Server.Subsz(opts *SubszOptions) (*Subsz, error)</c>.
    /// </summary>
    public (Subsz Result, Exception? Error) Subsz(SubszOptions? opts = null)
    {
        opts ??= new SubszOptions();

        var now   = DateTime.UtcNow;
        var total = new SublistStats();

        _mu.EnterReadLock();
        try
        {
            foreach (var kvp in _accounts)
            {
                var acc = kvp.Value;
                if (!string.IsNullOrEmpty(opts.Account) && acc.Name != opts.Account)
                    continue;
                if (acc.Sublist != null)
                    total.Add(acc.Sublist.Stats());
            }
        }
        finally
        {
            _mu.ExitReadLock();
        }

        var subsz = new Subsz
        {
            Id          = ID(),
            Now         = now,
            NumSubs     = total.NumSubs,
            NumCache    = total.NumCache,
            NumInserts  = total.NumInserts,
            NumRemoves  = total.NumRemoves,
            NumMatches  = total.NumMatches,
            CacheHitRate = total.CacheHitRate,
            MaxFanout   = total.MaxFanout,
            AvgFanout   = total.AvgFanout,
            Offset      = opts.Offset,
            Limit       = opts.Limit > 0 ? opts.Limit : MonitorDefaults.DefaultSubListSize,
        };

        return (subsz, null);
    }

    /// <summary>
    /// Handles the /subsz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleSubsz()</c>.
    /// </summary>
    public void HandleSubsz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Subsz);
        var q    = request.QueryString;
        var opts = new SubszOptions
        {
            Offset        = DecodeInt(q, "offset"),
            Limit         = DecodeInt(q, "limit", MonitorDefaults.DefaultSubListSize),
            Subscriptions = DecodeBool(q, "subs"),
            Account       = q["acc"],
            Test          = q["test"],
        };
        var (result, err) = Subsz(opts);
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(500, request, response, errBytes);
            return;
        }
        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Stacksz — /stacksz
    // =========================================================================

    /// <summary>
    /// Handles the /stacksz HTTP endpoint (returns all managed threads' stacks).
    /// Mirrors Go <c>Server.HandleStacksz()</c>.
    /// </summary>
    public void HandleStacksz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Stacksz);
        // In Go this returns goroutine stacks; in .NET we return thread info.
        var sb = new StringBuilder();
        sb.AppendLine("{\"stacks\":[");
        var threads = Process.GetCurrentProcess().Threads;
        bool first = true;
        foreach (ProcessThread t in threads)
        {
            if (!first) sb.Append(',');
            sb.AppendLine($"{{\"id\":{t.Id},\"state\":\"{t.ThreadState}\"}}");
            first = false;
        }
        sb.Append("]}");
        var data = Encoding.UTF8.GetBytes(sb.ToString());
        response.ContentType = "application/json";
        response.StatusCode  = 200;
        response.ContentLength64 = data.Length;
        response.OutputStream.Write(data);
        response.OutputStream.Close();
    }

    // =========================================================================
    // IP Queuesz — /ipqueuesz
    // =========================================================================

    /// <summary>
    /// Returns IP queue stats snapshot.
    /// Mirrors Go <c>Server.Ipqueuesz(opts *IpqueueszOptions) *IpqueueszStatus</c>.
    /// </summary>
    public IpqueueszStatus Ipqueuesz(IpqueueszOptions? opts = null)
    {
        // In .NET there is no direct equivalent of Go's ip queues;
        // return empty result mirroring Go behaviour when no queues exist.
        return new IpqueueszStatus();
    }

    /// <summary>
    /// Handles the /ipqueuesz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleIPQueuesz()</c>.
    /// </summary>
    public void HandleIPQueuesz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.IPQueuesz);
        var result = Ipqueuesz();
        var data   = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Varz — /varz
    // =========================================================================

    /// <summary>
    /// Creates and returns the initial static Varz snapshot (config fields).
    /// Mirrors Go <c>Server.createVarz()</c>.
    /// </summary>
    private Varz CreateVarz()
    {
        var opts = GetOpts();
        var varz = new Varz();

        // Static config fields
        _mu.EnterReadLock();
        varz.Id             = _info.Id;
        varz.Name           = _info.Name;
        varz.Version        = _info.Version;
        varz.Proto          = _info.Proto;
        varz.GitCommit      = _info.GitCommit;
        varz.GoVersion      = _info.GoVersion;
        varz.Host           = _info.Host;
        varz.Port           = _info.Port;
        varz.AuthRequired   = _info.AuthRequired;
        varz.TlsRequired    = _info.TlsRequired;
        varz.TlsVerify      = _info.TlsVerify;
        varz.TlsOcspPeerVerify = _ocspPeerVerify;
        varz.Ip             = _info.Ip;
        varz.ClientConnectUrls = _info.ClientConnectUrls?.ToList();
        varz.WsConnectUrls  = _info.WsConnectUrls?.ToList();
        varz.Start          = _start;
        varz.ConfigLoadTime = _configTime;
        _mu.ExitReadLock();

        // Options
        varz.MaxConn        = opts.MaxConn;
        varz.MaxSubs        = opts.MaxSubs;
        varz.PingIntervalNs = (long)opts.PingInterval.TotalMilliseconds * 1_000_000;
        varz.MaxPingsOut    = opts.MaxPingsOut;
        varz.HttpHost       = opts.HttpHost;
        varz.HttpPort       = opts.HttpPort;
        varz.HttpBasePath   = opts.HttpBasePath;
        varz.HttpsPort      = opts.HttpsPort;
        varz.AuthTimeout    = opts.AuthTimeout;
        varz.MaxControlLine = opts.MaxControlLine;
        varz.MaxPayload     = opts.MaxPayload;
        varz.MaxPending     = opts.MaxPending;
        varz.TlsTimeout     = opts.TlsTimeout;
        varz.WriteDeadlineNs = (long)opts.WriteDeadline.TotalMilliseconds * 1_000_000;
        varz.Tags           = opts.Tags.Count > 0 ? [.. opts.Tags] : null;

        // System account
        if (SystemAccount() is { } sysAcc)
            varz.SystemAccount = sysAcc.Name;

        // Cluster options
        varz.Cluster = new ClusterOptsVarz
        {
            Name        = opts.Cluster.Name,
            Host        = opts.Cluster.Host,
            Port        = opts.Cluster.Port,
            AuthTimeout = opts.Cluster.AuthTimeout,
            TlsTimeout  = opts.Cluster.TlsTimeout,
            TlsRequired = opts.Cluster.TlsConfig != null,
            PoolSize    = opts.Cluster.PoolSize,
        };
        if (opts.Routes is { Count: > 0 })
            varz.Cluster.Urls = [.. opts.Routes.Select(u => u.ToString())];

        // Gateway options
        varz.Gateway = new GatewayOptsVarz
        {
            Name            = opts.Gateway.Name,
            Host            = opts.Gateway.Host,
            Port            = opts.Gateway.Port,
            AuthTimeout     = opts.Gateway.AuthTimeout,
            TlsTimeout      = opts.Gateway.TlsTimeout,
            TlsRequired     = opts.Gateway.TlsConfig != null,
            Advertise       = opts.Gateway.Advertise,
            ConnectRetries  = opts.Gateway.ConnectRetries,
            RejectUnknown   = opts.Gateway.RejectUnknown,
        };
        if (opts.Gateway.Gateways is { Count: > 0 })
        {
            varz.Gateway.Gateways = [.. opts.Gateway.Gateways.Select(g => new RemoteGatewayOptsVarz
            {
                Name       = g.Name,
                TlsTimeout = g.TlsTimeout,
                Urls       = g.Urls?.Select(u => u.ToString()).ToList(),
            })];
        }

        // LeafNode options
        varz.LeafNode = new LeafNodeOptsVarz
        {
            Host        = opts.LeafNode.Host,
            Port        = opts.LeafNode.Port,
            AuthTimeout = opts.LeafNode.AuthTimeout,
            TlsTimeout  = opts.LeafNode.TlsTimeout,
            TlsRequired = opts.LeafNode.TlsConfig != null,
        };

        // MQTT options
        varz.Mqtt = new MqttOptsVarz
        {
            Host        = opts.Mqtt.Host,
            Port        = opts.Mqtt.Port,
            NoAuthUser  = opts.Mqtt.NoAuthUser,
            AuthTimeout = opts.Mqtt.AuthTimeout,
            TlsTimeout  = opts.Mqtt.TlsTimeout,
            JsDomain    = opts.Mqtt.JsDomain,
            AckWaitNs   = (long)opts.Mqtt.AckWait.TotalMilliseconds * 1_000_000,
            MaxAckPending = opts.Mqtt.MaxAckPending,
        };

        // WebSocket options
        varz.Websocket = new WebsocketOptsVarz
        {
            Host        = opts.Websocket.Host,
            Port        = opts.Websocket.Port,
            NoAuthUser  = opts.Websocket.NoAuthUser,
            AuthTimeout = opts.Websocket.AuthTimeout,
            NoTls       = opts.Websocket.NoTls,
            Compression = opts.Websocket.Compression,
            SameOrigin  = opts.Websocket.SameOrigin,
            AllowedOrigins = opts.Websocket.AllowedOrigins.Count > 0 ? opts.Websocket.AllowedOrigins : null,
        };

        return varz;
    }

    /// <summary>
    /// Updates the runtime (dynamic) fields on a <see cref="Varz"/> snapshot.
    /// Mirrors Go <c>Server.updateVarzRuntimeFields()</c>.
    /// </summary>
    private void UpdateVarzRuntimeFields(Varz varz)
    {
        var now = DateTime.UtcNow;
        varz.Now    = now;
        varz.Uptime = MyUptime(now - _start);

        // Memory
        var gcInfo = GC.GetGCMemoryInfo();
        varz.Mem  = gcInfo.MemoryLoadBytes;

        // CPU cores
        varz.Cores    = Environment.ProcessorCount;
        varz.MaxProcs = Environment.ProcessorCount;

        // Connection counts
        _mu.EnterReadLock();
        varz.Connections    = _clients.Count;
        varz.TotalConnections = _totalClients;
        varz.Routes         = NumRoutesInternal();
        varz.Remotes        = NumRemotesInternal();
        varz.Leafs          = _leafs.Count;
        _mu.ExitReadLock();

        // Stats
        varz.InMsgs          = Interlocked.Read(ref _stats.InMsgs);
        varz.OutMsgs         = Interlocked.Read(ref _stats.OutMsgs);
        varz.InBytes         = Interlocked.Read(ref _stats.InBytes);
        varz.OutBytes        = Interlocked.Read(ref _stats.OutBytes);
        varz.SlowConsumers   = Interlocked.Read(ref _stats.SlowConsumers);
        varz.StaleConnections = Interlocked.Read(ref _stats.StaleConnections);
        varz.StalledClients  = Interlocked.Read(ref _stats.Stalls);
        varz.Subscriptions   = NumSubscriptions();
        varz.PinnedAccountFail = (ulong)Interlocked.Read(ref _pinnedAccFail);

        // HTTP request stats
        _mu.EnterReadLock();
        varz.HttpReqStats = new Dictionary<string, ulong>(_httpReqStats);
        _mu.ExitReadLock();

        // Slow consumer / stale connection breakdowns
        varz.SlowConsumersStats = new SlowConsumersStats
        {
            Clients  = (ulong)Interlocked.Read(ref _scStats.Clients),
            Routes   = (ulong)Interlocked.Read(ref _scStats.Routes),
            Gateways = (ulong)Interlocked.Read(ref _scStats.Gateways),
            Leafs    = (ulong)Interlocked.Read(ref _scStats.Leafs),
        };
        varz.StaleConnectionStats = new StaleConnectionStats
        {
            Clients  = (ulong)Interlocked.Read(ref _staleStats.Clients),
            Routes   = (ulong)Interlocked.Read(ref _staleStats.Routes),
            Gateways = (ulong)Interlocked.Read(ref _staleStats.Gateways),
            Leafs    = (ulong)Interlocked.Read(ref _staleStats.Leafs),
        };
    }

    /// <summary>
    /// Returns a full server variable (Varz) snapshot.
    /// Mirrors Go <c>Server.Varz(opts *VarzOptions) (*Varz, error)</c>.
    /// </summary>
    public (Varz Result, Exception? Error) Varz(VarzOptions? opts = null)
    {
        var varz = CreateVarz();
        UpdateVarzRuntimeFields(varz);
        return (varz, null);
    }

    /// <summary>
    /// Handles the /varz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleVarz()</c>.
    /// </summary>
    public void HandleVarz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Varz);
        var (result, err) = Varz();
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(500, request, response, errBytes);
            return;
        }
        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Gatewayz — /gatewayz
    // =========================================================================

    /// <summary>
    /// Returns a gateway info snapshot.
    /// Mirrors Go <c>Server.Gatewayz(opts *GatewayzOptions) (*Gatewayz, error)</c>.
    /// </summary>
    public (Gatewayz Result, Exception? Error) Gatewayz(GatewayzOptions? opts = null)
    {
        opts ??= new GatewayzOptions();
        var now = DateTime.UtcNow;
        var gz  = new Gatewayz
        {
            Id  = ID(),
            Now = now,
        };

        var serverOpts = GetOpts();
        if (!string.IsNullOrEmpty(serverOpts.Gateway.Name))
        {
            gz.Name = serverOpts.Gateway.Name;
            gz.Host = serverOpts.Gateway.Host;
            gz.Port = serverOpts.Gateway.Port;
        }

        // Outbound gateways
        _mu.EnterReadLock();
        try
        {
            foreach (var kvp in _gateway.Out)
            {
                var gwName = kvp.Key;
                if (!string.IsNullOrEmpty(opts.Name) && gwName != opts.Name) continue;
                var c  = kvp.Value;
                var rg = new RemoteGatewayz
                {
                    IsConfigured = true,
                    Connection   = FillConnInfo(c, new ConnzOptions(), now),
                };
                gz.OutboundGateways[gwName] = rg;
            }

            // Inbound gateways
            foreach (var kvp in _gateway.In)
            {
                var c      = kvp.Value;
                var gwConn = c.Gateway;
                if (gwConn is null) continue;
                var gwName = gwConn.Name;
                if (!string.IsNullOrEmpty(opts.Name) && gwName != opts.Name) continue;

                if (!gz.InboundGateways.TryGetValue(gwName, out var inList))
                {
                    inList = [];
                    gz.InboundGateways[gwName] = inList;
                }
                inList.Add(new RemoteGatewayz
                {
                    Connection = FillConnInfo(c, new ConnzOptions(), now),
                });
            }
        }
        finally
        {
            _mu.ExitReadLock();
        }

        return (gz, null);
    }

    /// <summary>
    /// Handles the /gatewayz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleGatewayz()</c>.
    /// </summary>
    public void HandleGatewayz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Gatewayz);
        var q    = request.QueryString;
        var opts = new GatewayzOptions
        {
            Name        = q["gw_name"] ?? string.Empty,
            Accounts    = DecodeBool(q, "accs"),
            AccountName = q["acc_name"] ?? string.Empty,
        };
        var (result, err) = Gatewayz(opts);
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(500, request, response, errBytes);
            return;
        }
        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Leafz — /leafz
    // =========================================================================

    /// <summary>
    /// Returns a leaf node info snapshot.
    /// Mirrors Go <c>Server.Leafz(opts *LeafzOptions) (*Leafz, error)</c>.
    /// </summary>
    public (Leafz Result, Exception? Error) Leafz(LeafzOptions? opts = null)
    {
        opts ??= new LeafzOptions();
        var now   = DateTime.UtcNow;
        var leafs = new List<LeafInfo>();

        _mu.EnterReadLock();
        try
        {
            foreach (var kvp in _leafs)
            {
                var c = kvp.Value;
                lock (c)
                {
                    if (!string.IsNullOrEmpty(opts.Account))
                    {
                        var accName = (c._account as Account)?.Name ?? string.Empty;
                        if (accName != opts.Account) continue;
                    }

                    var li = new LeafInfo
                    {
                        Id      = c.Cid,
                        Name    = c.Opts.Name.Length > 0 ? c.Opts.Name : null,
                        Ip      = c.Host,
                        Port    = c.Port,
                        NumSubs = (uint)c.Subs.Count,
                    };

                    if (c._account is { } acc)
                        li.Account = acc.Name;

                    if (c.Rtt > TimeSpan.Zero)
                        li.Rtt = FormatRtt(c.Rtt);

                    if (c.Leaf is { } leaf)
                    {
                        li.IsSpoke   = leaf.IsSpoke;
                        li.IsIsolated = leaf.Isolated;
                        if (!string.IsNullOrEmpty(leaf.Compression))
                            li.Compression = leaf.Compression;
                    }

                    if (c.ProxyKey.Length > 0)
                        li.Proxy = new ProxyInfo { Key = c.ProxyKey };

                    if (opts.Subscriptions)
                        li.Subs = [.. c.Subs.Keys];

                    leafs.Add(li);
                }
            }
        }
        finally
        {
            _mu.ExitReadLock();
        }

        return (new Leafz
        {
            Id       = ID(),
            Now      = now,
            NumLeafs = leafs.Count,
            Leafs    = leafs,
        }, null);
    }

    /// <summary>
    /// Handles the /leafz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleLeafz()</c>.
    /// </summary>
    public void HandleLeafz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Leafz);
        var q    = request.QueryString;
        var opts = new LeafzOptions
        {
            Subscriptions = DecodeBool(q, "subs"),
            Account       = q["acc"] ?? string.Empty,
        };
        var (result, err) = Leafz(opts);
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(500, request, response, errBytes);
            return;
        }
        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // AccountStatz — /accstatz
    // =========================================================================

    /// <summary>
    /// Returns per-account stats.
    /// Mirrors Go <c>Server.AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error)</c>.
    /// </summary>
    public (AccountStatz Result, Exception? Error) AccountStatz(AccountStatzOptions? opts = null)
    {
        opts ??= new AccountStatzOptions();
        var now  = DateTime.UtcNow;
        var accs = new List<AccountStat>();

        _mu.EnterReadLock();
        try
        {
            foreach (var kvp in _accounts)
            {
                var acc = kvp.Value;
                if (opts.Accounts is { Count: > 0 } && !opts.Accounts.Contains(acc.Name))
                    continue;
                var conns = acc.NumLocalConnections();
                if (!opts.IncludeUnused && conns == 0) continue;

                accs.Add(new AccountStat
                {
                    Account  = acc.Name,
                    Name     = acc.Name,
                    Conns    = conns,
                    TotalConns = conns,
                    NumSubs  = (uint)acc.TotalSubs(),
                });
            }
        }
        finally
        {
            _mu.ExitReadLock();
        }

        return (new AccountStatz
        {
            Id       = ID(),
            Now      = now,
            Accounts = accs,
        }, null);
    }

    /// <summary>
    /// Handles the /accstatz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleAccountStatz()</c>.
    /// </summary>
    public void HandleAccountStatz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.AccountStatz);
        var q    = request.QueryString;
        var opts = new AccountStatzOptions
        {
            IncludeUnused = DecodeBool(q, "unused"),
        };
        var accParam = q["accs"] ?? string.Empty;
        if (!string.IsNullOrEmpty(accParam))
            opts.Accounts = [.. accParam.Split(',', StringSplitOptions.RemoveEmptyEntries)];

        var (result, err) = AccountStatz(opts);
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(500, request, response, errBytes);
            return;
        }
        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Accountz — /accountz
    // =========================================================================

    /// <summary>
    /// Returns account list or detail.
    /// Mirrors Go <c>Server.Accountz(opts *AccountzOptions) (*Accountz, error)</c>.
    /// </summary>
    public (Accountz Result, Exception? Error) Accountz(AccountzOptions? opts = null)
    {
        opts ??= new AccountzOptions();
        var now = DateTime.UtcNow;

        var az = new Accountz
        {
            Id  = ID(),
            Now = now,
        };

        if (SystemAccount() is { } sysAcc)
            az.SystemAccount = sysAcc.Name;

        if (!string.IsNullOrEmpty(opts.Account))
        {
            var (ai, err) = AccountInfo(opts.Account);
            if (err != null)
                return (az, err);
            az.Account = ai;
        }
        else
        {
            _mu.EnterReadLock();
            az.Accounts = [.. _accounts.Keys.OrderBy(k => k)];
            _mu.ExitReadLock();
        }

        return (az, null);
    }

    /// <summary>
    /// Returns detailed info for a single account.
    /// Mirrors Go <c>Server.accountInfo(accName string) (*AccountInfo, error)</c>.
    /// </summary>
    public (AccountInfo? Info, Exception? Error) AccountInfo(string accName)
    {
        var (acc, err) = LookupAccount(accName);
        if (err != null) return (null, err);
        if (acc is null)
            return (null, new InvalidOperationException($"account not found: {accName}"));

        var ai = new AccountInfo
        {
            AccountName = acc.Name,
            Complete    = true,
            JetStream   = acc.JetStream != null,
            ClientCnt   = acc.NumLocalConnections(),
            SubCnt      = (uint)acc.TotalSubs(),
        };

        if (acc.Sublist != null)
            ai.Sublist = acc.Sublist.Stats();

        if (SystemAccount() is { } sysAcc && sysAcc.Name == acc.Name)
            ai.IsSystem = true;

        return (ai, null);
    }

    /// <summary>
    /// Handles the /accountz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleAccountz()</c>.
    /// </summary>
    public void HandleAccountz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Accountz);
        var q    = request.QueryString;
        var opts = new AccountzOptions
        {
            Account = q["acc"] ?? string.Empty,
        };
        var (result, err) = Accountz(opts);
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(404, request, response, errBytes);
            return;
        }
        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Root — /
    // =========================================================================

    /// <summary>
    /// Handles the root HTTP endpoint (returns HTML with links to monitoring endpoints).
    /// Mirrors Go <c>Server.HandleRoot()</c>.
    /// </summary>
    public void HandleRoot(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Root);
        var html = $@"<html><head><title>NATS Monitor</title></head><body>
<h1>NATS Monitoring</h1>
<ul>
<li><a href=""{BasePath(MonitorPaths.Varz)}"">varz</a></li>
<li><a href=""{BasePath(MonitorPaths.Connz)}"">connz</a></li>
<li><a href=""{BasePath(MonitorPaths.Routez)}"">routez</a></li>
<li><a href=""{BasePath(MonitorPaths.Gatewayz)}"">gatewayz</a></li>
<li><a href=""{BasePath(MonitorPaths.Leafz)}"">leafz</a></li>
<li><a href=""{BasePath(MonitorPaths.Subsz)}"">subsz</a></li>
<li><a href=""{BasePath(MonitorPaths.Accountz)}"">accountz</a></li>
<li><a href=""{BasePath(MonitorPaths.AccountStatz)}"">accstatz</a></li>
<li><a href=""{BasePath(MonitorPaths.Healthz)}"">healthz</a></li>
<li><a href=""{BasePath(MonitorPaths.Raftz)}"">raftz</a></li>
<li><a href=""{BasePath(MonitorPaths.IPQueuesz)}"">ipqueuesz</a></li>
<li><a href=""{BasePath(MonitorPaths.Expvarz)}"">expvarz</a></li>
</ul></body></html>";

        var data = Encoding.UTF8.GetBytes(html);
        response.ContentType = "text/html";
        response.StatusCode  = 200;
        response.ContentLength64 = data.Length;
        response.OutputStream.Write(data);
        response.OutputStream.Close();
    }

    // =========================================================================
    // Healthz — /healthz
    // =========================================================================

    /// <summary>
    /// Internal health check implementation.
    /// Mirrors Go <c>Server.healthz(opts *HealthzOptions) *HealthStatus</c>.
    /// </summary>
    private HealthStatus InternalHealthz(HealthzOptions opts)
    {
        if (IsShuttingDown())
            return new HealthStatus
            {
                Status = "unavailable",
                Error  = "server shutdown",
            };

        if (opts.JSEnabled)
        {
            if (_jetStream is null)
                return new HealthStatus
                {
                    Status = "unavailable",
                    Error  = "jetstream not enabled",
                };
        }

        return new HealthStatus { Status = "ok" };
    }

    /// <summary>
    /// Returns the health status.
    /// Mirrors Go <c>Server.Healthz(opts *HealthzOptions) (*HealthStatus, error)</c>.
    /// </summary>
    public HealthStatus Healthz(HealthzOptions? opts = null)
        => InternalHealthz(opts ?? new HealthzOptions());

    /// <summary>
    /// Handles the /healthz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleHealthz()</c>.
    /// </summary>
    public void HandleHealthz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Healthz);
        var q    = request.QueryString;
        var opts = new HealthzOptions
        {
            JSEnabled         = DecodeBool(q, "js-enabled"),
            JSEnabledOnly     = DecodeBool(q, "js-not-leaf"),
            JSServerOnly      = DecodeBool(q, "js-server-only"),
            Account           = q["account"] ?? string.Empty,
            Details           = DecodeBool(q, "details"),
        };

        var hs   = Healthz(opts);
        var data = JsonSerializer.SerializeToUtf8Bytes(hs, MonitorJsonOptions);

        var code = hs.Status == "ok" ? 200
                 : hs.Error != null && hs.StatusCode == 400 ? 400
                 : hs.Error != null && hs.StatusCode == 404 ? 404
                 : hs.Status == "unavailable" ? 503
                 : 200;

        if (code == 200)
            ResponseHandler(request, response, data);
        else
            HandleResponse(code, request, response, data);
    }

    // =========================================================================
    // Raftz — /raftz
    // =========================================================================

    /// <summary>
    /// Returns Raft group info snapshot.
    /// Mirrors Go <c>Server.Raftz(opts *RaftzOptions) (*RaftzStatus, error)</c>.
    /// </summary>
    public (RaftzStatus Result, Exception? Error) Raftz(RaftzOptions? opts = null)
    {
        // Raft nodes are stored in _raftNodes. Return empty structure for now.
        var status = new RaftzStatus();
        return (status, null);
    }

    /// <summary>
    /// Handles the /raftz HTTP endpoint.
    /// Mirrors Go <c>Server.HandleRaftz()</c>.
    /// </summary>
    public void HandleRaftz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Raftz);
        var (result, err) = Raftz();
        if (err != null)
        {
            var errBytes = Encoding.UTF8.GetBytes($"{{\"error\":\"{err.Message}\"}}");
            HandleResponse(500, request, response, errBytes);
            return;
        }
        var data = JsonSerializer.SerializeToUtf8Bytes(result, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // Expvarz / Profilez — /debug/vars
    // =========================================================================

    /// <summary>
    /// Returns expvar-style diagnostics snapshot.
    /// Mirrors Go <c>Server.HandleExpvarz()</c>.
    /// </summary>
    public void HandleExpvarz(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        IncrHttpReqStat(MonitorPaths.Expvarz);

        // Build JSON manually using JsonDocument to match ExpvarzStatus structure.
        var cmdline  = Environment.GetCommandLineArgs();
        var memBytes = GC.GetGCMemoryInfo().MemoryLoadBytes;
        var totalAlloc = GC.GetTotalAllocatedBytes();
        var gcCycles = GC.CollectionCount(0);

        // Build a JSON object inline since ExpvarzStatus uses JsonElement? fields.
        var memObj = $"{{\"TotalAlloc\":{totalAlloc},\"HeapAlloc\":{memBytes},\"GcCycles\":{gcCycles}}}";
        var cmdArr = $"[{string.Join(",", cmdline.Select(a => JsonSerializer.Serialize(a)))}]";
        var json   = $"{{\"memstats\":{memObj},\"cmdline\":{cmdArr}}}";

        var data = Encoding.UTF8.GetBytes(json);
        ResponseHandler(request, response, data);
    }

    /// <summary>
    /// Handles the profilez endpoint.
    /// Mirrors Go <c>Server.HandleProfilez()</c>.
    /// </summary>
    public void HandleProfilez(System.Net.HttpListenerRequest request, System.Net.HttpListenerResponse response)
    {
        var status = new ProfilezStatus { Error = "not supported in .NET" };
        var data   = JsonSerializer.SerializeToUtf8Bytes(status, MonitorJsonOptions);
        ResponseHandler(request, response, data);
    }

    // =========================================================================
    // HTTP request stat tracking
    // =========================================================================

    /// <summary>
    /// Increments the HTTP request counter for a monitoring path.
    /// Mirrors Go <c>s.incrHttpReqStat(path string)</c>.
    /// </summary>
    private void IncrHttpReqStat(string path)
    {
        _mu.EnterWriteLock();
        try
        {
            _httpReqStats.TryGetValue(path, out var v);
            _httpReqStats[path] = v + 1;
        }
        finally
        {
            _mu.ExitWriteLock();
        }
    }

    // =========================================================================
    // Private helpers
    // =========================================================================

    /// <summary>
    /// Sorts a <see cref="ConnInfos"/> list by the given sort option.
    /// Mirrors Go sort logic in monitor_sort_opts.go.
    /// </summary>
    private static void SortConnInfos(ConnInfos list, SortOpt sort)
    {
        var now = DateTime.UtcNow;
        IComparer<ConnInfo> comparer = (string)sort switch
        {
            "start"      => Comparer<ConnInfo>.Create((x, y) => x.Start.CompareTo(y.Start)),
            "subs"       => SortBySubs.Instance,
            "pending"    => SortByPending.Instance,
            "msgs_to"    => SortByOutMsgs.Instance,
            "msgs_from"  => SortByInMsgs.Instance,
            "bytes_to"   => SortByOutBytes.Instance,
            "bytes_from" => SortByInBytes.Instance,
            "last"       => SortByLast.Instance,
            "idle"       => new SortByIdle(now),
            "uptime"     => new SortByUptime(now),
            "stop"       => SortByStop.Instance,
            "reason"     => SortByReason.Instance,
            "rtt"        => SortByRtt.Instance,
            _            => SortByCid.Instance,
        };
        list.Sort(comparer);
    }

    /// <summary>
    /// Formats a <see cref="TimeSpan"/> RTT value in Go style (e.g. "1.234ms").
    /// Mirrors Go <c>rtt.String()</c> for monitoring display.
    /// </summary>
    private static string FormatRtt(TimeSpan rtt)
    {
        var ns = (long)(rtt.TotalMilliseconds * 1_000_000);
        if (ns < 1000)            return $"{ns}ns";
        if (ns < 1_000_000)       return $"{ns / 1000.0:0.###}µs";
        if (ns < 1_000_000_000)   return $"{ns / 1_000_000.0:0.###}ms";
        return $"{ns / 1_000_000_000.0:0.###}s";
    }
}

// ============================================================================
// ClosedState.String() extension
// Mirrors Go <c>ClosedState.String()</c> in monitor.go.
// ============================================================================

/// <summary>
/// Extension methods for <see cref="ClosedState"/>.
/// Mirrors Go <c>ClosedState.String()</c> in server/monitor.go.
/// </summary>
public static class ClosedStateExtensions
{
    private static readonly Dictionary<ClosedState, string> _strs = new()
    {
        [ClosedState.ClientClosed]                     = "Client Closed",
        [ClosedState.AuthenticationTimeout]            = "Authentication Timeout",
        [ClosedState.AuthenticationViolation]          = "Authentication Failure",
        [ClosedState.TlsHandshakeError]                = "TLS Handshake Failure",
        [ClosedState.SlowConsumerPendingBytes]         = "Slow Consumer (Pending Bytes)",
        [ClosedState.SlowConsumerWriteDeadline]        = "Slow Consumer (Write Deadline)",
        [ClosedState.WriteError]                       = "Write Error",
        [ClosedState.ReadError]                        = "Read Error",
        [ClosedState.ParseError]                       = "Parse Error",
        [ClosedState.StaleConnection]                  = "Stale Connection",
        [ClosedState.ProtocolViolation]                = "Protocol Violation",
        [ClosedState.BadClientProtocolVersion]         = "Bad Client Protocol Version",
        [ClosedState.WrongPort]                        = "Incorrect Port",
        [ClosedState.MaxAccountConnectionsExceeded]    = "Maximum Account Connections Exceeded",
        [ClosedState.MaxConnectionsExceeded]           = "Maximum Connections Exceeded",
        [ClosedState.MaxPayloadExceeded]               = "Maximum Message Payload Exceeded",
        [ClosedState.MaxControlLineExceeded]           = "Maximum Control Line Exceeded",
        [ClosedState.MaxSubscriptionsExceeded]         = "Maximum Subscriptions Exceeded",
        [ClosedState.DuplicateRoute]                   = "Duplicate Route",
        [ClosedState.RouteRemoved]                     = "Route Removed",
        [ClosedState.ServerShutdown]                   = "Server Shutdown",
        [ClosedState.AuthenticationExpired]            = "Authentication Expired",
        [ClosedState.WrongGateway]                     = "Wrong Gateway",
        [ClosedState.MissingAccount]                   = "Missing Account",
        [ClosedState.Revocation]                       = "Revoked",
        [ClosedState.InternalClient]                   = "Internal Client",
        [ClosedState.MsgHeaderViolation]               = "Message Header Violation",
        [ClosedState.NoRespondersRequiresHeaders]      = "No Responders Requires Headers",
        [ClosedState.ClusterNameConflict]              = "Cluster Name Conflict",
        [ClosedState.DuplicateRemoteLeafnodeConnection] = "Duplicate Remote LeafNode Connection",
        [ClosedState.DuplicateClientId]                = "Duplicate Client ID",
        [ClosedState.DuplicateServerName]              = "Duplicate Server Name",
        [ClosedState.MinimumVersionRequired]           = "Minimum Version Required",
        [ClosedState.ClusterNamesIdentical]            = "Cluster Names Identical",
        [ClosedState.Kicked]                           = "Kicked",
        [ClosedState.ProxyNotTrusted]                  = "Proxy Not Trusted",
        [ClosedState.ProxyRequired]                    = "Proxy Required",
    };

    /// <summary>
    /// Returns a human-readable description of the closed state reason.
    /// Mirrors Go <c>ClosedState.String()</c> in server/monitor.go.
    /// </summary>
    public static string AsString(this ClosedState state)
        => _strs.TryGetValue(state, out var s) ? s : state.ToString();
}
