// 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);
    }

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

    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>
    /// 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>
/// 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 };
}
