// Port of Go server/reload.go — config diffing, validation, and CLI override merging
// for hot reload support. Reference: golang/nats-server/server/reload.go.

using System.Net.Security;
using NATS.Server.Tls;

namespace NATS.Server.Configuration;

/// <summary>
/// Provides static methods for comparing two <see cref="NatsOptions"/> instances,
/// validating that detected changes are reloadable, and merging CLI overrides
/// so that command-line flags always take precedence over config file values.
/// </summary>
public static class ConfigReloader
{
    // Non-reloadable options (match Go server — Host, Port, ServerName require restart)
    private static readonly HashSet<string> NonReloadable =
        ["Host", "Port", "ServerName", "Cluster", "JetStream.StoreDir"];

    // Logging-related options
    private static readonly HashSet<string> LoggingOptions =
        ["Debug", "Trace", "TraceVerbose", "Logtime", "LogtimeUTC", "LogFile",
         "LogSizeLimit", "LogMaxFiles", "Syslog", "RemoteSyslog"];

    // Auth-related options
    private static readonly HashSet<string> AuthOptions =
        ["Username", "Password", "Authorization", "Users", "NKeys",
         "NoAuthUser", "AuthTimeout", "Mqtt.Username", "Mqtt.Password"];

    // TLS-related options
    private static readonly HashSet<string> TlsOptions =
        ["TlsCert", "TlsKey", "TlsCaCert", "TlsVerify", "TlsMap",
         "TlsTimeout", "TlsHandshakeFirst", "TlsHandshakeFirstFallback",
         "AllowNonTls", "TlsRateLimit", "TlsPinnedCerts"];

    /// <summary>
    /// Compares two <see cref="NatsOptions"/> instances property by property and returns
    /// a list of <see cref="IConfigChange"/> for every property that differs. Each change
    /// is tagged with the appropriate category flags.
    /// </summary>
    public static List<IConfigChange> Diff(NatsOptions oldOpts, NatsOptions newOpts)
    {
        var changes = new List<IConfigChange>();

        // Non-reloadable
        CompareAndAdd(changes, "Host", oldOpts.Host, newOpts.Host);
        CompareAndAdd(changes, "Port", oldOpts.Port, newOpts.Port);
        CompareAndAdd(changes, "ServerName", oldOpts.ServerName, newOpts.ServerName);

        // Logging
        CompareAndAdd(changes, "Debug", oldOpts.Debug, newOpts.Debug);
        CompareAndAdd(changes, "Trace", oldOpts.Trace, newOpts.Trace);
        CompareAndAdd(changes, "TraceVerbose", oldOpts.TraceVerbose, newOpts.TraceVerbose);
        CompareAndAdd(changes, "Logtime", oldOpts.Logtime, newOpts.Logtime);
        CompareAndAdd(changes, "LogtimeUTC", oldOpts.LogtimeUTC, newOpts.LogtimeUTC);
        CompareAndAdd(changes, "LogFile", oldOpts.LogFile, newOpts.LogFile);
        CompareAndAdd(changes, "LogSizeLimit", oldOpts.LogSizeLimit, newOpts.LogSizeLimit);
        CompareAndAdd(changes, "LogMaxFiles", oldOpts.LogMaxFiles, newOpts.LogMaxFiles);
        CompareAndAdd(changes, "Syslog", oldOpts.Syslog, newOpts.Syslog);
        CompareAndAdd(changes, "RemoteSyslog", oldOpts.RemoteSyslog, newOpts.RemoteSyslog);

        // Auth
        CompareAndAdd(changes, "Username", oldOpts.Username, newOpts.Username);
        CompareAndAdd(changes, "Password", oldOpts.Password, newOpts.Password);
        CompareAndAdd(changes, "Authorization", oldOpts.Authorization, newOpts.Authorization);
        CompareCollectionAndAdd(changes, "Users", oldOpts.Users, newOpts.Users);
        CompareCollectionAndAdd(changes, "NKeys", oldOpts.NKeys, newOpts.NKeys);
        CompareAndAdd(changes, "NoAuthUser", oldOpts.NoAuthUser, newOpts.NoAuthUser);
        CompareAndAdd(changes, "AuthTimeout", oldOpts.AuthTimeout, newOpts.AuthTimeout);

        // TLS
        CompareAndAdd(changes, "TlsCert", oldOpts.TlsCert, newOpts.TlsCert);
        CompareAndAdd(changes, "TlsKey", oldOpts.TlsKey, newOpts.TlsKey);
        CompareAndAdd(changes, "TlsCaCert", oldOpts.TlsCaCert, newOpts.TlsCaCert);
        CompareAndAdd(changes, "TlsVerify", oldOpts.TlsVerify, newOpts.TlsVerify);
        CompareAndAdd(changes, "TlsMap", oldOpts.TlsMap, newOpts.TlsMap);
        CompareAndAdd(changes, "TlsTimeout", oldOpts.TlsTimeout, newOpts.TlsTimeout);
        CompareAndAdd(changes, "TlsHandshakeFirst", oldOpts.TlsHandshakeFirst, newOpts.TlsHandshakeFirst);
        CompareAndAdd(changes, "TlsHandshakeFirstFallback", oldOpts.TlsHandshakeFirstFallback, newOpts.TlsHandshakeFirstFallback);
        CompareAndAdd(changes, "AllowNonTls", oldOpts.AllowNonTls, newOpts.AllowNonTls);
        CompareAndAdd(changes, "TlsRateLimit", oldOpts.TlsRateLimit, newOpts.TlsRateLimit);
        CompareCollectionAndAdd(changes, "TlsPinnedCerts", oldOpts.TlsPinnedCerts, newOpts.TlsPinnedCerts);

        // Limits
        CompareAndAdd(changes, "MaxConnections", oldOpts.MaxConnections, newOpts.MaxConnections);
        CompareAndAdd(changes, "MaxPayload", oldOpts.MaxPayload, newOpts.MaxPayload);
        CompareAndAdd(changes, "MaxPending", oldOpts.MaxPending, newOpts.MaxPending);
        CompareAndAdd(changes, "WriteDeadline", oldOpts.WriteDeadline, newOpts.WriteDeadline);
        CompareAndAdd(changes, "PingInterval", oldOpts.PingInterval, newOpts.PingInterval);
        CompareAndAdd(changes, "MaxPingsOut", oldOpts.MaxPingsOut, newOpts.MaxPingsOut);
        CompareAndAdd(changes, "MaxControlLine", oldOpts.MaxControlLine, newOpts.MaxControlLine);
        CompareAndAdd(changes, "MaxSubs", oldOpts.MaxSubs, newOpts.MaxSubs);
        CompareAndAdd(changes, "MaxSubTokens", oldOpts.MaxSubTokens, newOpts.MaxSubTokens);
        CompareAndAdd(changes, "MaxTracedMsgLen", oldOpts.MaxTracedMsgLen, newOpts.MaxTracedMsgLen);
        CompareAndAdd(changes, "MaxClosedClients", oldOpts.MaxClosedClients, newOpts.MaxClosedClients);

        // Misc
        CompareCollectionAndAdd(changes, "Tags", oldOpts.Tags, newOpts.Tags);
        CompareAndAdd(changes, "LameDuckDuration", oldOpts.LameDuckDuration, newOpts.LameDuckDuration);
        CompareAndAdd(changes, "LameDuckGracePeriod", oldOpts.LameDuckGracePeriod, newOpts.LameDuckGracePeriod);
        CompareAndAdd(changes, "ClientAdvertise", oldOpts.ClientAdvertise, newOpts.ClientAdvertise);
        CompareAndAdd(changes, "DisableSublistCache", oldOpts.DisableSublistCache, newOpts.DisableSublistCache);
        CompareAndAdd(changes, "ConnectErrorReports", oldOpts.ConnectErrorReports, newOpts.ConnectErrorReports);
        CompareAndAdd(changes, "ReconnectErrorReports", oldOpts.ReconnectErrorReports, newOpts.ReconnectErrorReports);
        CompareAndAdd(changes, "NoHeaderSupport", oldOpts.NoHeaderSupport, newOpts.NoHeaderSupport);
        CompareAndAdd(changes, "NoSystemAccount", oldOpts.NoSystemAccount, newOpts.NoSystemAccount);
        CompareAndAdd(changes, "SystemAccount", oldOpts.SystemAccount, newOpts.SystemAccount);

        // MQTT runtime options
        if (oldOpts.Mqtt is null ^ newOpts.Mqtt is null)
            changes.Add(new ConfigChange("Mqtt"));
        else if (oldOpts.Mqtt is not null && newOpts.Mqtt is not null)
        {
            CompareAndAdd(changes, "Mqtt.Username", oldOpts.Mqtt.Username, newOpts.Mqtt.Username);
            CompareAndAdd(changes, "Mqtt.Password", oldOpts.Mqtt.Password, newOpts.Mqtt.Password);
            CompareAndAdd(changes, "Mqtt.AuthTimeout", oldOpts.Mqtt.AuthTimeout, newOpts.Mqtt.AuthTimeout);
            CompareAndAdd(changes, "Mqtt.AckWait", oldOpts.Mqtt.AckWait, newOpts.Mqtt.AckWait);
            CompareAndAdd(changes, "Mqtt.MaxAckPending", oldOpts.Mqtt.MaxAckPending, newOpts.Mqtt.MaxAckPending);
            CompareAndAdd(changes, "Mqtt.SessionPersistence", oldOpts.Mqtt.SessionPersistence, newOpts.Mqtt.SessionPersistence);
            CompareAndAdd(changes, "Mqtt.SessionTtl", oldOpts.Mqtt.SessionTtl, newOpts.Mqtt.SessionTtl);
            CompareAndAdd(changes, "Mqtt.Qos1PubAck", oldOpts.Mqtt.Qos1PubAck, newOpts.Mqtt.Qos1PubAck);
        }

        // Cluster and JetStream (restart-required boundaries)
        if (!ClusterEquivalent(oldOpts.Cluster, newOpts.Cluster))
            changes.Add(new ConfigChange("Cluster", isNonReloadable: true));

        if (JetStreamStoreDirChanged(oldOpts.JetStream, newOpts.JetStream))
            changes.Add(new ConfigChange("JetStream.StoreDir", isNonReloadable: true));

        return changes;
    }

    /// <summary>
    /// Validates a list of config changes and returns error messages for any
    /// non-reloadable changes (properties that require a server restart).
    /// </summary>
    public static List<string> Validate(List<IConfigChange> changes)
    {
        var errors = new List<string>();
        foreach (var change in changes)
        {
            if (change.IsNonReloadable)
            {
                errors.Add($"Config reload: '{change.Name}' cannot be changed at runtime (requires restart)");
            }
        }

        return errors;
    }

    /// <summary>
    /// Merges CLI overrides into a freshly-parsed config so that command-line flags
    /// always take precedence. Only properties whose names appear in <paramref name="cliFlags"/>
    /// are copied from <paramref name="cliValues"/> to <paramref name="fromConfig"/>.
    /// </summary>
    public static void MergeCliOverrides(NatsOptions fromConfig, NatsOptions cliValues, HashSet<string> cliFlags)
    {
        foreach (var flag in cliFlags)
        {
            switch (flag)
            {
                // Non-reloadable
                case "Host":
                    fromConfig.Host = cliValues.Host;
                    break;
                case "Port":
                    fromConfig.Port = cliValues.Port;
                    break;
                case "ServerName":
                    fromConfig.ServerName = cliValues.ServerName;
                    break;

                // Logging
                case "Debug":
                    fromConfig.Debug = cliValues.Debug;
                    break;
                case "Trace":
                    fromConfig.Trace = cliValues.Trace;
                    break;
                case "TraceVerbose":
                    fromConfig.TraceVerbose = cliValues.TraceVerbose;
                    break;
                case "Logtime":
                    fromConfig.Logtime = cliValues.Logtime;
                    break;
                case "LogtimeUTC":
                    fromConfig.LogtimeUTC = cliValues.LogtimeUTC;
                    break;
                case "LogFile":
                    fromConfig.LogFile = cliValues.LogFile;
                    break;
                case "LogSizeLimit":
                    fromConfig.LogSizeLimit = cliValues.LogSizeLimit;
                    break;
                case "LogMaxFiles":
                    fromConfig.LogMaxFiles = cliValues.LogMaxFiles;
                    break;
                case "Syslog":
                    fromConfig.Syslog = cliValues.Syslog;
                    break;
                case "RemoteSyslog":
                    fromConfig.RemoteSyslog = cliValues.RemoteSyslog;
                    break;

                // Auth
                case "Username":
                    fromConfig.Username = cliValues.Username;
                    break;
                case "Password":
                    fromConfig.Password = cliValues.Password;
                    break;
                case "Authorization":
                    fromConfig.Authorization = cliValues.Authorization;
                    break;
                case "Users":
                    fromConfig.Users = cliValues.Users;
                    break;
                case "NKeys":
                    fromConfig.NKeys = cliValues.NKeys;
                    break;
                case "NoAuthUser":
                    fromConfig.NoAuthUser = cliValues.NoAuthUser;
                    break;
                case "AuthTimeout":
                    fromConfig.AuthTimeout = cliValues.AuthTimeout;
                    break;

                // TLS
                case "TlsCert":
                    fromConfig.TlsCert = cliValues.TlsCert;
                    break;
                case "TlsKey":
                    fromConfig.TlsKey = cliValues.TlsKey;
                    break;
                case "TlsCaCert":
                    fromConfig.TlsCaCert = cliValues.TlsCaCert;
                    break;
                case "TlsVerify":
                    fromConfig.TlsVerify = cliValues.TlsVerify;
                    break;
                case "TlsMap":
                    fromConfig.TlsMap = cliValues.TlsMap;
                    break;
                case "TlsTimeout":
                    fromConfig.TlsTimeout = cliValues.TlsTimeout;
                    break;
                case "TlsHandshakeFirst":
                    fromConfig.TlsHandshakeFirst = cliValues.TlsHandshakeFirst;
                    break;
                case "TlsHandshakeFirstFallback":
                    fromConfig.TlsHandshakeFirstFallback = cliValues.TlsHandshakeFirstFallback;
                    break;
                case "AllowNonTls":
                    fromConfig.AllowNonTls = cliValues.AllowNonTls;
                    break;
                case "TlsRateLimit":
                    fromConfig.TlsRateLimit = cliValues.TlsRateLimit;
                    break;
                case "TlsPinnedCerts":
                    fromConfig.TlsPinnedCerts = cliValues.TlsPinnedCerts;
                    break;

                // Limits
                case "MaxConnections":
                    fromConfig.MaxConnections = cliValues.MaxConnections;
                    break;
                case "MaxPayload":
                    fromConfig.MaxPayload = cliValues.MaxPayload;
                    break;
                case "MaxPending":
                    fromConfig.MaxPending = cliValues.MaxPending;
                    break;
                case "WriteDeadline":
                    fromConfig.WriteDeadline = cliValues.WriteDeadline;
                    break;
                case "PingInterval":
                    fromConfig.PingInterval = cliValues.PingInterval;
                    break;
                case "MaxPingsOut":
                    fromConfig.MaxPingsOut = cliValues.MaxPingsOut;
                    break;
                case "MaxControlLine":
                    fromConfig.MaxControlLine = cliValues.MaxControlLine;
                    break;
                case "MaxSubs":
                    fromConfig.MaxSubs = cliValues.MaxSubs;
                    break;
                case "MaxSubTokens":
                    fromConfig.MaxSubTokens = cliValues.MaxSubTokens;
                    break;
                case "MaxTracedMsgLen":
                    fromConfig.MaxTracedMsgLen = cliValues.MaxTracedMsgLen;
                    break;
                case "MaxClosedClients":
                    fromConfig.MaxClosedClients = cliValues.MaxClosedClients;
                    break;

                // Misc
                case "Tags":
                    fromConfig.Tags = cliValues.Tags;
                    break;
                case "LameDuckDuration":
                    fromConfig.LameDuckDuration = cliValues.LameDuckDuration;
                    break;
                case "LameDuckGracePeriod":
                    fromConfig.LameDuckGracePeriod = cliValues.LameDuckGracePeriod;
                    break;
                case "ClientAdvertise":
                    fromConfig.ClientAdvertise = cliValues.ClientAdvertise;
                    break;
                case "DisableSublistCache":
                    fromConfig.DisableSublistCache = cliValues.DisableSublistCache;
                    break;
                case "ConnectErrorReports":
                    fromConfig.ConnectErrorReports = cliValues.ConnectErrorReports;
                    break;
                case "ReconnectErrorReports":
                    fromConfig.ReconnectErrorReports = cliValues.ReconnectErrorReports;
                    break;
                case "NoHeaderSupport":
                    fromConfig.NoHeaderSupport = cliValues.NoHeaderSupport;
                    break;
                case "NoSystemAccount":
                    fromConfig.NoSystemAccount = cliValues.NoSystemAccount;
                    break;
                case "SystemAccount":
                    fromConfig.SystemAccount = cliValues.SystemAccount;
                    break;
            }
        }
    }

    /// <summary>
    /// Applies a validated set of config changes by copying reloadable property values
    /// from <paramref name="newOpts"/> to <paramref name="currentOpts"/>. Returns category
    /// flags indicating which subsystems need to be notified.
    /// Reference: Go server/reload.go — applyOptions.
    /// </summary>
    public static ConfigApplyResult ApplyDiff(
        List<IConfigChange> changes,
        NatsOptions currentOpts,
        NatsOptions newOpts)
    {
        bool hasLoggingChanges = false;
        bool hasAuthChanges = false;
        bool hasTlsChanges = false;

        foreach (var change in changes)
        {
            if (change.IsLoggingChange) hasLoggingChanges = true;
            if (change.IsAuthChange) hasAuthChanges = true;
            if (change.IsTlsChange) hasTlsChanges = true;
        }

        return new ConfigApplyResult(
            HasLoggingChanges: hasLoggingChanges,
            HasAuthChanges: hasAuthChanges,
            HasTlsChanges: hasTlsChanges,
            ChangeCount: changes.Count);
    }

    /// <summary>
    /// Asynchronous reload entry point that parses the config file, diffs against
    /// current options, validates changes, and returns the result. The caller (typically
    /// the SIGHUP handler) is responsible for applying the result to the running server.
    /// Reference: Go server/reload.go — Reload.
    /// </summary>
    public static async Task<ConfigReloadResult> ReloadAsync(
        string configFile,
        NatsOptions currentOpts,
        string? currentDigest,
        NatsOptions? cliSnapshot,
        HashSet<string> cliFlags,
        CancellationToken ct = default)
    {
        return await Task.Run(() =>
        {
            var (newConfig, digest) = NatsConfParser.ParseFileWithDigest(configFile);
            if (digest == currentDigest)
                return new ConfigReloadResult(Unchanged: true);

            var newOpts = new NatsOptions { ConfigFile = configFile };
            ConfigProcessor.ApplyConfig(newConfig, newOpts);

            if (cliSnapshot != null)
                MergeCliOverrides(newOpts, cliSnapshot, cliFlags);

            var changes = Diff(currentOpts, newOpts);
            var errors = Validate(changes);

            return new ConfigReloadResult(
                Unchanged: false,
                NewOptions: newOpts,
                NewDigest: digest,
                Changes: changes,
                Errors: errors);
        }, ct);
    }

    /// <summary>
    /// Compares two options directly (without reading from a config file) and returns
    /// a reload result indicating whether the change is valid.
    /// Go reference: server/reload.go — Reload with in-memory options comparison.
    /// </summary>
    public static Task<ReloadFromOptionsResult> ReloadFromOptionsAsync(NatsOptions original, NatsOptions updated)
    {
        var changes = Diff(original, updated);
        var errors = Validate(changes);
        var rejectedChanges = new List<string>();

        foreach (var change in changes)
        {
            if (change.IsNonReloadable)
            {
                rejectedChanges.Add($"{change.Name} cannot be changed at runtime");
            }
        }

        return Task.FromResult(new ReloadFromOptionsResult(
            Success: rejectedChanges.Count == 0 && errors.Count == 0,
            RejectedChanges: rejectedChanges));
    }

    /// <summary>
    /// Compares cluster-topology settings between two <see cref="NatsOptions"/> instances
    /// and returns a <see cref="ClusterConfigChangeResult"/> describing what changed.
    /// Callers use this to reconcile route/gateway/leaf connections after a hot reload.
    /// Reference: golang/nats-server/server/reload.go — routesOption.Apply / gatewayOption.Apply.
    /// </summary>
    public static ClusterConfigChangeResult ApplyClusterConfigChanges(NatsOptions oldOpts, NatsOptions newOpts)
    {
        var result = new ClusterConfigChangeResult();

        // Compare cluster route URLs (ClusterOptions.Routes)
        var oldRoutes = oldOpts.Cluster?.Routes ?? [];
        var newRoutes = newOpts.Cluster?.Routes ?? [];

        var oldSet = new HashSet<string>(oldRoutes, StringComparer.OrdinalIgnoreCase);
        var newSet = new HashSet<string>(newRoutes, StringComparer.OrdinalIgnoreCase);

        if (!oldSet.SetEquals(newSet))
        {
            result.RouteUrlsChanged = true;
            result.AddedRouteUrls.AddRange(newSet.Except(oldSet, StringComparer.OrdinalIgnoreCase));
            result.RemovedRouteUrls.AddRange(oldSet.Except(newSet, StringComparer.OrdinalIgnoreCase));
        }

        // Compare gateway remote URLs — flatten all URLs across all RemoteGateway entries
        var oldGwUrls = FlattenGatewayUrls(oldOpts.Gateway);
        var newGwUrls = FlattenGatewayUrls(newOpts.Gateway);
        if (!new HashSet<string>(oldGwUrls, StringComparer.OrdinalIgnoreCase)
                .SetEquals(new HashSet<string>(newGwUrls, StringComparer.OrdinalIgnoreCase)))
            result.GatewayUrlsChanged = true;

        // Compare leaf-node remote URLs — flatten Remotes + all RemoteLeaves entries
        var oldLeafUrls = FlattenLeafUrls(oldOpts.LeafNode);
        var newLeafUrls = FlattenLeafUrls(newOpts.LeafNode);
        if (!new HashSet<string>(oldLeafUrls, StringComparer.OrdinalIgnoreCase)
                .SetEquals(new HashSet<string>(newLeafUrls, StringComparer.OrdinalIgnoreCase)))
            result.LeafUrlsChanged = true;

        result.HasChanges = result.RouteUrlsChanged || result.GatewayUrlsChanged || result.LeafUrlsChanged;
        return result;
    }

    /// <summary>
    /// Compares logging settings between two <see cref="NatsOptions"/> instances and
    /// returns a <see cref="LoggingChangeResult"/> describing what changed.
    /// The effective log level is derived from the Debug/Trace flags: Trace → "Trace",
    /// Debug → "Debug", otherwise "Information" — matching Go's precedence.
    /// Reference: golang/nats-server/server/reload.go — traceOption.Apply / debugOption.Apply.
    /// </summary>
    public static LoggingChangeResult ApplyLoggingChanges(NatsOptions oldOpts, NatsOptions newOpts)
    {
        var result = new LoggingChangeResult();

        // Derive effective level from Debug/Trace flags (Trace takes precedence over Debug)
        var oldLevel = EffectiveLogLevel(oldOpts);
        var newLevel = EffectiveLogLevel(newOpts);

        if (!string.Equals(oldLevel, newLevel, StringComparison.OrdinalIgnoreCase))
        {
            result.LevelChanged = true;
            result.OldLevel = oldLevel;
            result.NewLevel = newLevel;
        }

        // Check individual trace/debug flags
        if (oldOpts.Trace != newOpts.Trace)
            result.TraceChanged = true;
        if (oldOpts.Debug != newOpts.Debug)
            result.DebugChanged = true;

        result.HasChanges = result.LevelChanged || result.TraceChanged || result.DebugChanged;
        return result;
    }

    // ─── Comparison helpers ─────────────────────────────────────────

    /// <summary>
    /// Derives the effective log-level string from Debug/Trace flags.
    /// Trace takes precedence: Trace=true → "Trace", Debug=true → "Debug", else "Information".
    /// </summary>
    private static string EffectiveLogLevel(NatsOptions opts)
    {
        if (opts.Trace) return "Trace";
        if (opts.Debug) return "Debug";
        return "Information";
    }

    /// <summary>Flattens all gateway remote URLs into a single list.</summary>
    private static List<string> FlattenGatewayUrls(GatewayOptions? gw)
    {
        if (gw is null) return [];
        var urls = new List<string>(gw.Remotes);
        foreach (var remote in gw.RemoteGateways)
            urls.AddRange(remote.Urls);
        return urls;
    }

    /// <summary>Flattens all leaf-node remote URLs into a single list.</summary>
    private static List<string> FlattenLeafUrls(LeafNodeOptions? leaf)
    {
        if (leaf is null) return [];
        var urls = new List<string>(leaf.Remotes);
        foreach (var remote in leaf.RemoteLeaves)
            urls.AddRange(remote.Urls);
        return urls;
    }

    private static void CompareAndAdd<T>(List<IConfigChange> changes, string name, T oldVal, T newVal)
    {
        if (!Equals(oldVal, newVal))
        {
            changes.Add(new ConfigChange(
                name,
                isLoggingChange: LoggingOptions.Contains(name),
                isAuthChange: AuthOptions.Contains(name),
                isTlsChange: TlsOptions.Contains(name),
                isNonReloadable: NonReloadable.Contains(name)));
        }
    }

    private static void CompareCollectionAndAdd<T>(List<IConfigChange> changes, string name, T? oldVal, T? newVal)
        where T : class
    {
        // For collections we compare by reference and null state.
        // A change from null to non-null (or vice versa), or a different reference, counts as changed.
        if (ReferenceEquals(oldVal, newVal))
            return;

        if (oldVal is null || newVal is null || !ReferenceEquals(oldVal, newVal))
        {
            changes.Add(new ConfigChange(
                name,
                isLoggingChange: LoggingOptions.Contains(name),
                isAuthChange: AuthOptions.Contains(name),
                isTlsChange: TlsOptions.Contains(name),
                isNonReloadable: NonReloadable.Contains(name)));
        }
    }

    private static bool ClusterEquivalent(ClusterOptions? oldCluster, ClusterOptions? newCluster)
    {
        if (oldCluster is null && newCluster is null)
            return true;

        if (oldCluster is null || newCluster is null)
            return false;

        if (!string.Equals(oldCluster.Name, newCluster.Name, StringComparison.Ordinal))
            return false;

        if (!string.Equals(oldCluster.Host, newCluster.Host, StringComparison.Ordinal))
            return false;

        if (oldCluster.Port != newCluster.Port)
            return false;

        return oldCluster.Routes.SequenceEqual(newCluster.Routes, StringComparer.Ordinal);
    }

    private static bool JetStreamStoreDirChanged(JetStreamOptions? oldJetStream, JetStreamOptions? newJetStream)
    {
        if (oldJetStream is null && newJetStream is null)
            return false;

        if (oldJetStream is null || newJetStream is null)
            return true;

        return !string.Equals(oldJetStream.StoreDir, newJetStream.StoreDir, StringComparison.Ordinal);
    }

    /// <summary>
    /// Compares auth-related fields between two <see cref="NatsOptions"/> instances and
    /// returns an <see cref="AuthChangeResult"/> describing what changed. This drives
    /// re-evaluation of existing connections after a config reload.
    /// Reference: golang/nats-server/server/reload.go — authOption.Apply / usersOption.Apply.
    /// </summary>
    public static AuthChangeResult PropagateAuthChanges(NatsOptions oldOpts, NatsOptions newOpts)
    {
        var result = new AuthChangeResult();

        // Check if users changed
        var oldUsers = oldOpts.Users;
        var newUsers = newOpts.Users;
        if ((oldUsers?.Count ?? 0) != (newUsers?.Count ?? 0))
        {
            result.UsersChanged = true;
        }
        else if (oldUsers is not null && newUsers is not null)
        {
            var oldUserSet = new HashSet<string>(oldUsers.Select(u => u.Username), StringComparer.Ordinal);
            var newUserSet = new HashSet<string>(newUsers.Select(u => u.Username), StringComparer.Ordinal);
            if (!oldUserSet.SetEquals(newUserSet))
                result.UsersChanged = true;
        }

        // Check if accounts changed
        var oldAccountCount = oldOpts.Accounts?.Count ?? 0;
        var newAccountCount = newOpts.Accounts?.Count ?? 0;
        if (oldAccountCount != newAccountCount)
            result.AccountsChanged = true;

        // Check auth token (Authorization is the token string directly on NatsOptions)
        if (!string.Equals(oldOpts.Authorization, newOpts.Authorization, StringComparison.Ordinal))
            result.TokenChanged = true;

        result.HasChanges = result.UsersChanged || result.AccountsChanged || result.TokenChanged;
        return result;
    }

    /// <summary>
    /// Compares TLS certificate paths between old and new config.
    /// If changed, validates the new cert is loadable.
    /// Go reference: server/reload.go — tlsConfigReload.
    /// </summary>
    public static TlsReloadResult ReloadTlsCertificates(NatsOptions oldOpts, NatsOptions newOpts)
    {
        var result = new TlsReloadResult();

        var oldCert = oldOpts.TlsCert ?? string.Empty;
        var newCert = newOpts.TlsCert ?? string.Empty;

        if (string.Equals(oldCert, newCert, StringComparison.Ordinal))
            return result; // No change

        result.CertificateChanged = true;
        result.CertificatePath = newCert;

        if (string.IsNullOrWhiteSpace(newCert))
        {
            result.CertificateLoaded = true; // TLS being disabled
            return result;
        }

        // Validate the file exists (actual X509 loading would require the key file too)
        if (!File.Exists(newCert))
        {
            result.Error = $"TLS certificate file not found: {newCert}";
            return result;
        }

        result.CertificateLoaded = true;
        return result;
    }

    /// <summary>
    /// Reloads TLS certificates from the current options and atomically swaps them
    /// into the certificate provider. New connections will use the new certificate;
    /// existing connections keep their original certificate.
    /// Reference: golang/nats-server/server/reload.go — tlsOption.Apply.
    /// </summary>
    public static bool ReloadTlsCertificate(
        NatsOptions options,
        TlsCertificateProvider? certProvider)
    {
        if (certProvider == null || !options.HasTls)
            return false;

        var oldCert = certProvider.SwapCertificate(options.TlsCert!, options.TlsKey);
        oldCert?.Dispose();

        // Rebuild SslServerAuthenticationOptions with the new certificate
        var newSslOptions = TlsHelper.BuildServerAuthOptions(options);
        certProvider.SwapSslOptions(newSslOptions);

        return true;
    }

    /// <summary>
    /// Compares JetStream configuration between two <see cref="NatsOptions"/> instances and
    /// returns a <see cref="JetStreamConfigChangeResult"/> describing what changed. This drives
    /// live reconfiguration of JetStream memory limits, file store limits, and domain after a
    /// hot reload without requiring a server restart.
    /// Reference: golang/nats-server/server/reload.go — jetStreamOption.Apply.
    /// </summary>
    public static JetStreamConfigChangeResult ApplyJetStreamConfigChanges(NatsOptions oldOpts, NatsOptions newOpts)
    {
        var result = new JetStreamConfigChangeResult();

        var oldJs = oldOpts.JetStream;
        var newJs = newOpts.JetStream;

        // Compare JetStream MaxMemoryStore (bytes)
        var oldMem = oldJs?.MaxMemoryStore ?? 0L;
        var newMem = newJs?.MaxMemoryStore ?? 0L;
        if (oldMem != newMem)
        {
            result.MaxMemoryChanged = true;
            result.OldMaxMemory = oldMem;
            result.NewMaxMemory = newMem;
        }

        // Compare JetStream MaxFileStore (bytes)
        var oldStore = oldJs?.MaxFileStore ?? 0L;
        var newStore = newJs?.MaxFileStore ?? 0L;
        if (oldStore != newStore)
        {
            result.MaxStoreChanged = true;
            result.OldMaxStore = oldStore;
            result.NewMaxStore = newStore;
        }

        // Compare JetStream Domain
        var oldDomain = oldJs?.Domain ?? string.Empty;
        var newDomain = newJs?.Domain ?? string.Empty;
        if (!string.Equals(oldDomain, newDomain, StringComparison.Ordinal))
        {
            result.DomainChanged = true;
            result.OldDomain = oldDomain;
            result.NewDomain = newDomain;
        }

        result.HasChanges = result.MaxMemoryChanged || result.MaxStoreChanged || result.DomainChanged;
        return result;
    }
}

/// <summary>
/// Result of applying a config diff — flags indicating which subsystems need notification.
/// </summary>
public readonly record struct ConfigApplyResult(
    bool HasLoggingChanges,
    bool HasAuthChanges,
    bool HasTlsChanges,
    int ChangeCount);

/// <summary>
/// Result of an async config reload operation. Contains the parsed options, diff, and
/// validation errors (if any). If <see cref="Unchanged"/> is true, no reload is needed.
/// </summary>
public sealed class ConfigReloadResult
{
    public bool Unchanged { get; }
    public NatsOptions? NewOptions { get; }
    public string? NewDigest { get; }
    public List<IConfigChange>? Changes { get; }
    public List<string>? Errors { get; }

    public ConfigReloadResult(
        bool Unchanged,
        NatsOptions? NewOptions = null,
        string? NewDigest = null,
        List<IConfigChange>? Changes = null,
        List<string>? Errors = null)
    {
        this.Unchanged = Unchanged;
        this.NewOptions = NewOptions;
        this.NewDigest = NewDigest;
        this.Changes = Changes;
        this.Errors = Errors;
    }

    public bool HasErrors => Errors is { Count: > 0 };
}

/// <summary>
/// Result of an in-memory options comparison for reload validation.
/// </summary>
public sealed record ReloadFromOptionsResult(bool Success, List<string> RejectedChanges);

/// <summary>
/// Describes what cluster-topology configuration changed between two <see cref="NatsOptions"/>
/// instances. Returned by <see cref="ConfigReloader.ApplyClusterConfigChanges"/> and used to
/// determine which routes, gateways, or leaf connections need to be reconciled after a reload.
/// Reference: golang/nats-server/server/reload.go — clusterOption.Apply / routesOption.Apply.
/// </summary>
public sealed class ClusterConfigChangeResult
{
    /// <summary>True when any cluster-topology field changed.</summary>
    public bool HasChanges { get; set; }

    /// <summary>True when the set of cluster route URLs changed.</summary>
    public bool RouteUrlsChanged { get; set; }

    /// <summary>True when any gateway remote URL set changed.</summary>
    public bool GatewayUrlsChanged { get; set; }

    /// <summary>True when the leaf-node remote URL set changed.</summary>
    public bool LeafUrlsChanged { get; set; }

    /// <summary>Route URLs present in new config but absent in old config.</summary>
    public List<string> AddedRouteUrls { get; init; } = [];

    /// <summary>Route URLs present in old config but absent in new config.</summary>
    public List<string> RemovedRouteUrls { get; init; } = [];
}

/// <summary>
/// Describes what logging configuration changed between two <see cref="NatsOptions"/>
/// instances. Returned by <see cref="ConfigReloader.ApplyLoggingChanges"/>.
/// Reference: golang/nats-server/server/reload.go — traceOption.Apply / debugOption.Apply.
/// </summary>
public sealed class LoggingChangeResult
{
    /// <summary>True when any logging field changed.</summary>
    public bool HasChanges { get; set; }

    /// <summary>True when the effective log level string changed.</summary>
    public bool LevelChanged { get; set; }

    /// <summary>The effective log level before the reload.</summary>
    public string? OldLevel { get; set; }

    /// <summary>The effective log level after the reload.</summary>
    public string? NewLevel { get; set; }

    /// <summary>True when the Trace flag changed.</summary>
    public bool TraceChanged { get; set; }

    /// <summary>True when the Debug flag changed.</summary>
    public bool DebugChanged { get; set; }
}

/// <summary>
/// Describes what auth-related configuration changed between two <see cref="NatsOptions"/>
/// instances. Returned by <see cref="ConfigReloader.PropagateAuthChanges"/> and used to
/// determine which existing connections need to be re-evaluated after a reload.
/// Reference: golang/nats-server/server/reload.go — authOption.Apply / usersOption.Apply.
/// </summary>
public sealed class AuthChangeResult
{
    /// <summary>True when any auth field changed.</summary>
    public bool HasChanges { get; set; }

    /// <summary>True when the set of configured users changed.</summary>
    public bool UsersChanged { get; set; }

    /// <summary>True when the set of configured accounts changed.</summary>
    public bool AccountsChanged { get; set; }

    /// <summary>True when the Authorization token string changed.</summary>
    public bool TokenChanged { get; set; }
}

/// <summary>
/// Describes what JetStream configuration changed between two <see cref="NatsOptions"/>
/// instances. Returned by <see cref="ConfigReloader.ApplyJetStreamConfigChanges"/> and used to
/// drive live reconfiguration of JetStream limits and domain after a hot reload.
/// Reference: golang/nats-server/server/reload.go — jetStreamOption.Apply.
/// </summary>
public sealed class JetStreamConfigChangeResult
{
    /// <summary>True when any JetStream field changed.</summary>
    public bool HasChanges { get; set; }

    /// <summary>True when the maximum memory store limit changed.</summary>
    public bool MaxMemoryChanged { get; set; }

    /// <summary>True when the maximum file store limit changed.</summary>
    public bool MaxStoreChanged { get; set; }

    /// <summary>True when the JetStream domain name changed.</summary>
    public bool DomainChanged { get; set; }

    /// <summary>Previous MaxMemoryStore value in bytes (0 = unlimited).</summary>
    public long OldMaxMemory { get; set; }

    /// <summary>New MaxMemoryStore value in bytes (0 = unlimited).</summary>
    public long NewMaxMemory { get; set; }

    /// <summary>Previous MaxFileStore value in bytes (0 = unlimited).</summary>
    public long OldMaxStore { get; set; }

    /// <summary>New MaxFileStore value in bytes (0 = unlimited).</summary>
    public long NewMaxStore { get; set; }

    /// <summary>Previous domain name (empty string when not configured).</summary>
    public string? OldDomain { get; set; }

    /// <summary>New domain name (empty string when not configured).</summary>
    public string? NewDomain { get; set; }
}

/// <summary>
/// Result of a TLS certificate path comparison and validation during config hot-reload.
/// Returned by <see cref="ConfigReloader.ReloadTlsCertificates"/>.
/// Go reference: server/reload.go — tlsConfigReload.
/// </summary>
public sealed class TlsReloadResult
{
    /// <summary>True when the TLS certificate path changed between old and new config.</summary>
    public bool CertificateChanged { get; set; }

    /// <summary>True when the new certificate path was successfully validated (or TLS is being disabled).</summary>
    public bool CertificateLoaded { get; set; }

    /// <summary>Set when validation fails (e.g., file not found). Null on success.</summary>
    public string? Error { get; set; }

    /// <summary>The new certificate file path (empty string when TLS is being disabled).</summary>
    public string? CertificatePath { get; set; }

    /// <summary>Reserved for future use: expiry time of the loaded certificate.</summary>
    public DateTime? ExpiresUtc { get; set; }
}
