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

namespace ZB.MOM.NatsNet.Server;

/// <summary>
/// JetStream API level versioning constants and helpers.
/// Mirrors server/jetstream_versioning.go.
/// </summary>
public static class JetStreamVersioning
{
    /// <summary>Maximum supported JetStream API level for this server.</summary>
    public const int JsApiLevel = 3;

    /// <summary>Metadata key that carries the required API level for a stream or consumer asset.</summary>
    public const string JsRequiredLevelMetadataKey = "_nats.req.level";

    /// <summary>Metadata key that carries the server version that created/updated the asset.</summary>
    public const string JsServerVersionMetadataKey = "_nats.ver";

    /// <summary>Metadata key that carries the server API level that created/updated the asset.</summary>
    public const string JsServerLevelMetadataKey = "_nats.level";

    // ---- API level feature gates ----
    // These document which API level each feature requires.
    // They correspond to the requires() calls in setStaticStreamMetadata / setStaticConsumerMetadata.

    /// <summary>API level required for per-message TTL and SubjectDeleteMarkerTTL (v2.11).</summary>
    public const int ApiLevelForTTL = 1;

    /// <summary>API level required for consumer PauseUntil (v2.11).</summary>
    public const int ApiLevelForConsumerPause = 1;

    /// <summary>API level required for priority groups (v2.11).</summary>
    public const int ApiLevelForPriorityGroups = 1;

    /// <summary>API level required for counter CRDTs (v2.12).</summary>
    public const int ApiLevelForCounters = 2;

    /// <summary>API level required for atomic batch publishing (v2.12).</summary>
    public const int ApiLevelForAtomicPublish = 2;

    /// <summary>API level required for message scheduling (v2.12).</summary>
    public const int ApiLevelForMsgSchedules = 2;

    /// <summary>API level required for async persist mode (v2.12).</summary>
    public const int ApiLevelForAsyncPersist = 2;

    // ---- Helper methods ----

    /// <summary>
    /// Returns the required API level string from stream or consumer metadata,
    /// or an empty string if not set.
    /// Mirrors <c>getRequiredApiLevel</c>.
    /// </summary>
    public static string GetRequiredApiLevel(IDictionary<string, string>? metadata)
    {
        if (metadata is not null && metadata.TryGetValue(JsRequiredLevelMetadataKey, out var l) && l.Length > 0)
            return l;
        return string.Empty;
    }

    /// <summary>
    /// Returns whether this server supports the required API level encoded in the asset's metadata.
    /// Mirrors <c>supportsRequiredApiLevel</c>.
    /// </summary>
    public static bool SupportsRequiredApiLevel(IDictionary<string, string>? metadata)
    {
        var l = GetRequiredApiLevel(metadata);
        if (l.Length == 0) return true;
        return int.TryParse(l, out var level) && level <= JsApiLevel;
    }

    /// <summary>
    /// Removes dynamic (per-response) versioning fields from metadata.
    /// These should never be stored; only added in API responses.
    /// Mirrors <c>deleteDynamicMetadata</c>.
    /// </summary>
    public static void DeleteDynamicMetadata(IDictionary<string, string> metadata)
    {
        metadata.Remove(JsServerVersionMetadataKey);
        metadata.Remove(JsServerLevelMetadataKey);
    }

    /// <summary>
    /// Returns whether a request should be rejected based on the Nats-Required-Api-Level header value.
    /// Mirrors <c>errorOnRequiredApiLevel</c>.
    /// </summary>
    public static bool ErrorOnRequiredApiLevel(string? reqApiLevelHeader)
    {
        if (string.IsNullOrEmpty(reqApiLevelHeader)) return false;
        return !int.TryParse(reqApiLevelHeader, out var minLevel) || JsApiLevel < minLevel;
    }

    // ---- Stream metadata mutations ----

    /// <summary>
    /// Sets the required API level in stream config metadata based on which v2.11+/v2.12+ features
    /// the stream config uses. Removes any dynamic fields first.
    /// Mirrors <c>setStaticStreamMetadata</c>.
    /// </summary>
    public static void SetStaticStreamMetadata(StreamConfig cfg)
    {
        cfg.Metadata ??= new Dictionary<string, string>();
        DeleteDynamicMetadata(cfg.Metadata);

        var requiredApiLevel = 0;
        void Requires(int level) { if (level > requiredApiLevel) requiredApiLevel = level; }

        if (cfg.AllowMsgTTL || cfg.SubjectDeleteMarkerTTL > TimeSpan.Zero)
            Requires(ApiLevelForTTL);
        if (cfg.AllowMsgCounter)
            Requires(ApiLevelForCounters);
        if (cfg.AllowAtomicPublish)
            Requires(ApiLevelForAtomicPublish);
        if (cfg.AllowMsgSchedules)
            Requires(ApiLevelForMsgSchedules);
        if (cfg.PersistMode == PersistModeType.AsyncPersistMode)
            Requires(ApiLevelForAsyncPersist);

        cfg.Metadata[JsRequiredLevelMetadataKey] = requiredApiLevel.ToString();
    }

    /// <summary>
    /// Returns a shallow copy of the stream config with dynamic versioning fields added to a new
    /// metadata dictionary. Does not mutate <paramref name="cfg"/>.
    /// Mirrors <c>setDynamicStreamMetadata</c>.
    /// </summary>
    public static StreamConfig SetDynamicStreamMetadata(StreamConfig cfg)
    {
        // Shallow-copy the struct-like record: clone all fields then replace metadata.
        var newCfg = cfg.Clone();
        newCfg.Metadata = new Dictionary<string, string>();
        if (cfg.Metadata != null)
            foreach (var kv in cfg.Metadata)
                newCfg.Metadata[kv.Key] = kv.Value;
        newCfg.Metadata[JsServerVersionMetadataKey] = ServerConstants.Version;
        newCfg.Metadata[JsServerLevelMetadataKey]   = JsApiLevel.ToString();
        return newCfg;
    }

    /// <summary>
    /// Copies the required-level versioning field from <paramref name="prevCfg"/> into
    /// <paramref name="cfg"/>, removing dynamic fields and deleting the key if absent in prevCfg.
    /// Mirrors <c>copyStreamMetadata</c>.
    /// </summary>
    public static void CopyStreamMetadata(StreamConfig cfg, StreamConfig? prevCfg)
    {
        if (cfg.Metadata != null)
            DeleteDynamicMetadata(cfg.Metadata);
        SetOrDeleteInStreamMetadata(cfg, prevCfg, JsRequiredLevelMetadataKey);
    }

    private static void SetOrDeleteInStreamMetadata(StreamConfig cfg, StreamConfig? prevCfg, string key)
    {
        if (prevCfg?.Metadata != null && prevCfg.Metadata.TryGetValue(key, out var value))
        {
            cfg.Metadata ??= new Dictionary<string, string>();
            cfg.Metadata[key] = value;
            return;
        }
        if (cfg.Metadata != null)
        {
            cfg.Metadata.Remove(key);
            if (cfg.Metadata.Count == 0)
                cfg.Metadata = null;
        }
    }

    // ---- Consumer metadata mutations ----

    /// <summary>
    /// Sets the required API level in consumer config metadata based on which v2.11+ features
    /// the consumer config uses. Removes any dynamic fields first.
    /// Mirrors <c>setStaticConsumerMetadata</c>.
    /// </summary>
    public static void SetStaticConsumerMetadata(ConsumerConfig cfg)
    {
        cfg.Metadata ??= new Dictionary<string, string>();
        DeleteDynamicMetadata(cfg.Metadata);

        var requiredApiLevel = 0;
        void Requires(int level) { if (level > requiredApiLevel) requiredApiLevel = level; }

        if (cfg.PauseUntil.HasValue && cfg.PauseUntil.Value != default)
            Requires(ApiLevelForConsumerPause);
        if (cfg.PriorityPolicy != PriorityPolicy.PriorityNone
            || cfg.PinnedTTL != TimeSpan.Zero
            || (cfg.PriorityGroups != null && cfg.PriorityGroups.Length > 0))
            Requires(ApiLevelForPriorityGroups);

        cfg.Metadata[JsRequiredLevelMetadataKey] = requiredApiLevel.ToString();
    }

    /// <summary>
    /// Returns a shallow copy of the consumer config with dynamic versioning fields added to a new
    /// metadata dictionary. Does not mutate <paramref name="cfg"/>.
    /// Mirrors <c>setDynamicConsumerMetadata</c>.
    /// </summary>
    public static ConsumerConfig SetDynamicConsumerMetadata(ConsumerConfig cfg)
    {
        var newCfg = new ConsumerConfig();
        // Copy all fields via serialisation-free approach: copy properties from cfg
        CopyConsumerConfigFields(cfg, newCfg);
        newCfg.Metadata = new Dictionary<string, string>();
        if (cfg.Metadata != null)
            foreach (var kv in cfg.Metadata)
                newCfg.Metadata[kv.Key] = kv.Value;
        newCfg.Metadata[JsServerVersionMetadataKey] = ServerConstants.Version;
        newCfg.Metadata[JsServerLevelMetadataKey]   = JsApiLevel.ToString();
        return newCfg;
    }

    /// <summary>
    /// Returns a shallow copy of the consumer info with dynamic versioning fields added to the
    /// config's metadata. Does not mutate <paramref name="info"/>.
    /// Mirrors <c>setDynamicConsumerInfoMetadata</c>.
    /// </summary>
    public static ConsumerInfo SetDynamicConsumerInfoMetadata(ConsumerInfo info)
    {
        var newInfo = new ConsumerInfo
        {
            Stream          = info.Stream,
            Name            = info.Name,
            Created         = info.Created,
            Delivered       = info.Delivered,
            AckFloor        = info.AckFloor,
            NumAckPending   = info.NumAckPending,
            NumRedelivered  = info.NumRedelivered,
            NumWaiting      = info.NumWaiting,
            NumPending      = info.NumPending,
            Cluster         = info.Cluster,
            PushBound       = info.PushBound,
            Paused          = info.Paused,
            PauseRemaining  = info.PauseRemaining,
            TimeStamp       = info.TimeStamp,
            PriorityGroups  = info.PriorityGroups,
            Config          = info.Config != null ? SetDynamicConsumerMetadata(info.Config) : null,
        };
        return newInfo;
    }

    /// <summary>
    /// Copies the required-level versioning field from <paramref name="prevCfg"/> into
    /// <paramref name="cfg"/>, removing dynamic fields and deleting the key if absent in prevCfg.
    /// Mirrors <c>copyConsumerMetadata</c>.
    /// </summary>
    public static void CopyConsumerMetadata(ConsumerConfig cfg, ConsumerConfig? prevCfg)
    {
        if (cfg.Metadata != null)
            DeleteDynamicMetadata(cfg.Metadata);
        SetOrDeleteInConsumerMetadata(cfg, prevCfg, JsRequiredLevelMetadataKey);
    }

    private static void SetOrDeleteInConsumerMetadata(ConsumerConfig cfg, ConsumerConfig? prevCfg, string key)
    {
        if (prevCfg?.Metadata != null && prevCfg.Metadata.TryGetValue(key, out var value))
        {
            cfg.Metadata ??= new Dictionary<string, string>();
            cfg.Metadata[key] = value;
            return;
        }
        if (cfg.Metadata != null)
        {
            cfg.Metadata.Remove(key);
            if (cfg.Metadata.Count == 0)
                cfg.Metadata = null;
        }
    }

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

    /// <summary>
    /// Copies all scalar/reference properties from <paramref name="src"/> to <paramref name="dst"/>,
    /// excluding <c>Metadata</c> (which is set separately by the caller).
    /// </summary>
    private static void CopyConsumerConfigFields(ConsumerConfig src, ConsumerConfig dst)
    {
        dst.DeliverPolicy        = src.DeliverPolicy;
        dst.OptStartSeq          = src.OptStartSeq;
        dst.OptStartTime         = src.OptStartTime;
        dst.DeliverSubject       = src.DeliverSubject;
        dst.DeliverGroup         = src.DeliverGroup;
        dst.Durable              = src.Durable;
        dst.Name                 = src.Name;
        dst.Description          = src.Description;
        dst.FilterSubject        = src.FilterSubject;
        dst.FilterSubjects       = src.FilterSubjects;
        dst.AckPolicy            = src.AckPolicy;
        dst.AckWait              = src.AckWait;
        dst.MaxDeliver           = src.MaxDeliver;
        dst.BackOff              = src.BackOff;
        dst.ReplayPolicy         = src.ReplayPolicy;
        dst.RateLimit            = src.RateLimit;
        dst.SampleFrequency      = src.SampleFrequency;
        dst.MaxWaiting           = src.MaxWaiting;
        dst.MaxAckPending        = src.MaxAckPending;
        dst.FlowControl          = src.FlowControl;
        dst.Heartbeat            = src.Heartbeat;
        dst.Direct               = src.Direct;
        dst.HeadersOnly          = src.HeadersOnly;
        dst.MaxRequestBatch      = src.MaxRequestBatch;
        dst.MaxRequestMaxBytes   = src.MaxRequestMaxBytes;
        dst.MaxRequestExpires    = src.MaxRequestExpires;
        dst.InactiveThreshold    = src.InactiveThreshold;
        dst.Replicas             = src.Replicas;
        dst.MemoryStorage        = src.MemoryStorage;
        dst.PauseUntil           = src.PauseUntil;
        dst.PinnedTTL            = src.PinnedTTL;
        dst.PriorityPolicy       = src.PriorityPolicy;
        dst.PriorityGroups       = src.PriorityGroups;
        // Metadata is NOT copied here — caller sets it.
    }
}
