using System.Collections.Concurrent;
using NATS.Server.JetStream.Models;

namespace NATS.Server.JetStream.Cluster;

/// <summary>
/// Orchestrates cluster-wide stream/consumer lifecycle via RAFT proposals.
/// The meta-group tracks StreamAssignment and ConsumerAssignment dictionaries,
/// validates proposals, and dispatches applied entries.
/// Go reference: jetstream_cluster.go:500-2000 (processStreamAssignment, processConsumerAssignment).
/// </summary>
public sealed class JetStreamMetaGroup
{
    private readonly int _nodes;
    private int _selfIndex;

    // Backward-compatible stream name set used by existing GetState().Streams.
    private readonly ConcurrentDictionary<string, byte> _streams = new(StringComparer.Ordinal);

    // Full StreamAssignment tracking for proposal workflow.
    // Go reference: jetstream_cluster.go streamAssignment, consumerAssignment maps.
    private readonly ConcurrentDictionary<string, StreamAssignment> _assignments =
        new(StringComparer.Ordinal);

    // Known peers in this cluster — used by ProcessAddPeer / ProcessRemovePeer.
    // Go reference: jetstream_cluster.go peer tracking in jetStreamCluster.
    private readonly HashSet<string> _knownPeers = new(StringComparer.Ordinal);

    // Account-scoped inflight proposal tracking -- entries proposed but not yet committed.
    // Go reference: jetstream_cluster.go inflight tracking for proposals (jetstream_cluster.go:1193-1278).
    // Outer key: account name. Inner key: stream name → InflightInfo.
    private readonly ConcurrentDictionary<string, Dictionary<string, InflightInfo>> _inflightStreams = new(StringComparer.Ordinal);
    // Outer key: account name. Inner key: "stream/consumer" → InflightInfo.
    private readonly ConcurrentDictionary<string, Dictionary<string, InflightInfo>> _inflightConsumers = new(StringComparer.Ordinal);

    // Running count of consumers across all stream assignments.
    private int _totalConsumerCount;

    private int _leaderIndex = 1;
    private long _leadershipVersion = 1;

    // Count of entries skipped due to unsupported version or unknown type.
    // Go reference: jetstream_cluster.go — forward-compatibility skip counter.
    private int _skippedUnsupportedEntries;

    public JetStreamMetaGroup(int nodes)
        : this(nodes, selfIndex: 1)
    {
    }

    public JetStreamMetaGroup(int nodes, int selfIndex)
    {
        _nodes = nodes;
        _selfIndex = selfIndex;
    }

    /// <summary>
    /// The highest assignment schema version this node supports.
    /// Assignments with a higher version are skipped rather than crashing the cluster.
    /// Go reference: jetstream_cluster.go — versioned assignment handling for forward compatibility.
    /// </summary>
    public const int CurrentVersion = 1;

    /// <summary>
    /// Returns true when this node is the current meta-group leader.
    /// Go reference: jetstream_api.go:200-300 -- leader check before mutating operations.
    /// </summary>
    public bool IsLeader() => _leaderIndex == _selfIndex;

    /// <summary>
    /// Simulates this node winning the leader election after a stepdown.
    /// Used in single-process test fixtures where only one "node" exists.
    /// Go reference: jetstream_cluster.go — after stepdown, a new leader is elected.
    /// </summary>
    public void BecomeLeader() => _selfIndex = _leaderIndex;

    /// <summary>
    /// Returns the leader identifier string, e.g. "meta-1".
    /// Used to populate the leader_hint field in not-leader error responses.
    /// </summary>
    public string Leader => $"meta-{_leaderIndex}";

    /// <summary>
    /// Number of streams currently tracked.
    /// </summary>
    public int StreamCount => _assignments.Count;

    /// <summary>
    /// Number of consumers across all streams.
    /// </summary>
    public int ConsumerCount => _totalConsumerCount;

    /// <summary>
    /// Number of entries skipped because their version exceeds <see cref="CurrentVersion"/>
    /// or their entry type is unknown. Allows operators to detect version skew in a mixed cluster.
    /// Go reference: jetstream_cluster.go — forward-compatibility skip tracking.
    /// </summary>
    public int SkippedUnsupportedEntries => _skippedUnsupportedEntries;

    /// <summary>
    /// Total number of inflight stream proposals across all accounts.
    /// </summary>
    public int InflightStreamCount => _inflightStreams.Values.Sum(d => d.Count);

    /// <summary>
    /// Total number of inflight consumer proposals across all accounts.
    /// </summary>
    public int InflightConsumerCount => _inflightConsumers.Values.Sum(d => d.Count);

    // ---------------------------------------------------------------
    // Inflight proposal tracking — public API
    // Go reference: jetstream_cluster.go:1193-1278 inflight proposal management.
    // ---------------------------------------------------------------

    /// <summary>
    /// Tracks a stream proposal as inflight for the given account.
    /// Increments OpsCount on duplicate proposals for the same stream name.
    /// Go reference: jetstream_cluster.go inflight proposal tracking.
    /// </summary>
    public void TrackInflightStreamProposal(string account, StreamAssignment sa)
    {
        var accountDict = _inflightStreams.GetOrAdd(account, _ => new Dictionary<string, InflightInfo>(StringComparer.Ordinal));
        lock (accountDict)
        {
            if (accountDict.TryGetValue(sa.StreamName, out var existing))
                accountDict[sa.StreamName] = existing with { OpsCount = existing.OpsCount + 1 };
            else
                accountDict[sa.StreamName] = new InflightInfo(OpsCount: 1, Deleted: false, Assignment: sa);
        }
    }

    /// <summary>
    /// Decrements OpsCount for a stream proposal. Removes the entry when OpsCount reaches zero.
    /// Removes the account entry when its dictionary becomes empty.
    /// Go reference: jetstream_cluster.go inflight proposal tracking.
    /// </summary>
    public void RemoveInflightStreamProposal(string account, string streamName)
    {
        if (!_inflightStreams.TryGetValue(account, out var accountDict))
            return;

        lock (accountDict)
        {
            if (!accountDict.TryGetValue(streamName, out var existing))
                return;

            if (existing.OpsCount <= 1)
            {
                accountDict.Remove(streamName);
                if (accountDict.Count == 0)
                    _inflightStreams.TryRemove(account, out _);
            }
            else
            {
                accountDict[streamName] = existing with { OpsCount = existing.OpsCount - 1 };
            }
        }
    }

    /// <summary>
    /// Returns true if the given stream is currently tracked as inflight for the account.
    /// Go reference: jetstream_cluster.go inflight check.
    /// </summary>
    public bool IsStreamInflight(string account, string streamName)
    {
        if (!_inflightStreams.TryGetValue(account, out var accountDict))
            return false;

        lock (accountDict)
        {
            return accountDict.TryGetValue(streamName, out var info) && info.OpsCount > 0;
        }
    }

    /// <summary>
    /// Tracks a consumer proposal as inflight for the given account.
    /// Increments OpsCount on duplicate proposals for the same stream/consumer key.
    /// Go reference: jetstream_cluster.go inflight consumer proposal tracking.
    /// </summary>
    public void TrackInflightConsumerProposal(string account, string streamName, string consumerName, ConsumerAssignment? ca = null)
    {
        var key = $"{streamName}/{consumerName}";
        var accountDict = _inflightConsumers.GetOrAdd(account, _ => new Dictionary<string, InflightInfo>(StringComparer.Ordinal));
        lock (accountDict)
        {
            if (accountDict.TryGetValue(key, out var existing))
                accountDict[key] = existing with { OpsCount = existing.OpsCount + 1 };
            else
                accountDict[key] = new InflightInfo(OpsCount: 1, Deleted: false, Assignment: null);
        }
    }

    /// <summary>
    /// Decrements OpsCount for a consumer proposal. Removes the entry when OpsCount reaches zero.
    /// Removes the account entry when its dictionary becomes empty.
    /// Go reference: jetstream_cluster.go inflight consumer proposal tracking.
    /// </summary>
    public void RemoveInflightConsumerProposal(string account, string streamName, string consumerName)
    {
        var key = $"{streamName}/{consumerName}";
        if (!_inflightConsumers.TryGetValue(account, out var accountDict))
            return;

        lock (accountDict)
        {
            if (!accountDict.TryGetValue(key, out var existing))
                return;

            if (existing.OpsCount <= 1)
            {
                accountDict.Remove(key);
                if (accountDict.Count == 0)
                    _inflightConsumers.TryRemove(account, out _);
            }
            else
            {
                accountDict[key] = existing with { OpsCount = existing.OpsCount - 1 };
            }
        }
    }

    /// <summary>
    /// Returns true if the given consumer is currently tracked as inflight for the account.
    /// Go reference: jetstream_cluster.go inflight check.
    /// </summary>
    public bool IsConsumerInflight(string account, string streamName, string consumerName)
    {
        var key = $"{streamName}/{consumerName}";
        if (!_inflightConsumers.TryGetValue(account, out var accountDict))
            return false;

        lock (accountDict)
        {
            return accountDict.TryGetValue(key, out var info) && info.OpsCount > 0;
        }
    }

    /// <summary>
    /// Clears all inflight stream and consumer proposals across all accounts.
    /// Go reference: jetstream_cluster.go — inflight cleared on shutdown/reset.
    /// </summary>
    public void ClearAllInflight()
    {
        _inflightStreams.Clear();
        _inflightConsumers.Clear();
    }

    // ---------------------------------------------------------------
    // Stream proposals
    // ---------------------------------------------------------------

    /// <summary>
    /// Proposes creating a stream. Stores in both the backward-compatible name set
    /// and the full assignment map.
    /// Go reference: jetstream_cluster.go processStreamAssignment.
    /// </summary>
    public Task ProposeCreateStreamAsync(StreamConfig config, CancellationToken ct)
        => ProposeCreateStreamAsync(config, group: null, ct);

    /// <summary>
    /// Proposes creating a stream with an explicit RAFT group assignment.
    /// Idempotent: duplicate creates for the same name are silently ignored.
    /// Go reference: jetstream_cluster.go processStreamAssignment.
    /// </summary>
    public Task ProposeCreateStreamAsync(StreamConfig config, RaftGroup? group, CancellationToken ct)
    {
        _ = ct;

        var resolvedGroup = group ?? new RaftGroup { Name = config.Name };

        // Track as inflight
        TrackInflightStreamProposal("$G", new StreamAssignment { StreamName = config.Name, Group = resolvedGroup });

        // Apply the entry (idempotent via AddOrUpdate)
        ApplyStreamCreate(config.Name, resolvedGroup);

        // Clear inflight
        RemoveInflightStreamProposal("$G", config.Name);

        return Task.CompletedTask;
    }

    /// <summary>
    /// Proposes creating a stream with leader validation and duplicate rejection.
    /// Use this method when the caller needs strict validation (e.g. API layer).
    /// Go reference: jetstream_cluster.go processStreamAssignment with validation.
    /// </summary>
    public Task ProposeCreateStreamValidatedAsync(StreamConfig config, RaftGroup? group, CancellationToken ct)
    {
        _ = ct;

        if (!IsLeader())
            throw new InvalidOperationException($"Not the meta-group leader. Current leader: {Leader}");

        if (_assignments.ContainsKey(config.Name))
            throw new InvalidOperationException($"Stream '{config.Name}' already exists.");

        var resolvedGroup = group ?? new RaftGroup { Name = config.Name };

        // Track as inflight
        TrackInflightStreamProposal("$G", new StreamAssignment { StreamName = config.Name, Group = resolvedGroup });

        // Apply the entry
        ApplyStreamCreate(config.Name, resolvedGroup);

        // Clear inflight
        RemoveInflightStreamProposal("$G", config.Name);

        return Task.CompletedTask;
    }

    /// <summary>
    /// Proposes deleting a stream. Removes from both tracking structures.
    /// Go reference: jetstream_cluster.go processStreamDelete.
    /// </summary>
    public Task ProposeDeleteStreamAsync(string streamName, CancellationToken ct)
    {
        _ = ct;
        ApplyStreamDelete(streamName);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Proposes deleting a stream with leader validation.
    /// Go reference: jetstream_cluster.go processStreamDelete with leader check.
    /// </summary>
    public Task ProposeDeleteStreamValidatedAsync(string streamName, CancellationToken ct)
    {
        _ = ct;

        if (!IsLeader())
            throw new InvalidOperationException($"Not the meta-group leader. Current leader: {Leader}");

        ApplyStreamDelete(streamName);
        return Task.CompletedTask;
    }

    // ---------------------------------------------------------------
    // Consumer proposals
    // ---------------------------------------------------------------

    /// <summary>
    /// Proposes creating a consumer assignment within a stream.
    /// If the stream does not exist, the consumer is silently not tracked.
    /// Go reference: jetstream_cluster.go processConsumerAssignment.
    /// </summary>
    public Task ProposeCreateConsumerAsync(
        string streamName,
        string consumerName,
        RaftGroup group,
        CancellationToken ct)
    {
        _ = ct;

        // Track as inflight
        TrackInflightConsumerProposal("$G", streamName, consumerName);

        // Apply the entry (silently ignored if stream does not exist)
        ApplyConsumerCreate(streamName, consumerName, group);

        // Clear inflight
        RemoveInflightConsumerProposal("$G", streamName, consumerName);

        return Task.CompletedTask;
    }

    /// <summary>
    /// Proposes creating a consumer with leader and stream-existence validation.
    /// Use this method when the caller needs strict validation (e.g. API layer).
    /// Go reference: jetstream_cluster.go processConsumerAssignment with validation.
    /// </summary>
    public Task ProposeCreateConsumerValidatedAsync(
        string streamName,
        string consumerName,
        RaftGroup group,
        CancellationToken ct)
    {
        _ = ct;

        if (!IsLeader())
            throw new InvalidOperationException($"Not the meta-group leader. Current leader: {Leader}");

        if (!_assignments.ContainsKey(streamName))
            throw new InvalidOperationException($"Stream '{streamName}' not found.");

        // Track as inflight
        TrackInflightConsumerProposal("$G", streamName, consumerName);

        // Apply the entry
        ApplyConsumerCreate(streamName, consumerName, group);

        // Clear inflight
        RemoveInflightConsumerProposal("$G", streamName, consumerName);

        return Task.CompletedTask;
    }

    /// <summary>
    /// Proposes deleting a consumer assignment from a stream.
    /// Silently does nothing if stream or consumer does not exist.
    /// Go reference: jetstream_cluster.go processConsumerDelete.
    /// </summary>
    public Task ProposeDeleteConsumerAsync(
        string streamName,
        string consumerName,
        CancellationToken ct)
    {
        _ = ct;
        ApplyConsumerDelete(streamName, consumerName);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Proposes deleting a consumer with leader validation.
    /// Go reference: jetstream_cluster.go processConsumerDelete with leader check.
    /// </summary>
    public Task ProposeDeleteConsumerValidatedAsync(
        string streamName,
        string consumerName,
        CancellationToken ct)
    {
        _ = ct;

        if (!IsLeader())
            throw new InvalidOperationException($"Not the meta-group leader. Current leader: {Leader}");

        ApplyConsumerDelete(streamName, consumerName);
        return Task.CompletedTask;
    }

    // ---------------------------------------------------------------
    // Validated assignment processing
    // Go reference: jetstream_cluster.go:4541-5925
    // ---------------------------------------------------------------

    /// <summary>
    /// Validates and processes a stream assignment.
    /// Returns false if the assignment is invalid (empty name, null group) or if
    /// the assignment's version exceeds <see cref="CurrentVersion"/> (forward-compatibility skip).
    /// Version 0 is treated as version 1 for backward compatibility with pre-versioned entries.
    /// Idempotent: duplicate assignments for the same stream name are accepted.
    /// Go reference: jetstream_cluster.go:4541 processStreamAssignment.
    /// </summary>
    public bool ProcessStreamAssignment(StreamAssignment sa)
    {
        if (string.IsNullOrEmpty(sa.StreamName) || sa.Group == null)
            return false;

        // Version 0 means pre-versioned (treat as current). Versions above CurrentVersion
        // are from a newer node and must be skipped to avoid corrupting cluster state.
        var effectiveVersion = sa.Version == 0 ? CurrentVersion : sa.Version;
        if (effectiveVersion > CurrentVersion)
        {
            Interlocked.Increment(ref _skippedUnsupportedEntries);
            return false;
        }

        AddStreamAssignment(sa);
        return true;
    }

    /// <summary>
    /// Updates an existing stream assignment's configuration.
    /// Returns false if the stream does not exist.
    /// Go reference: jetstream_cluster.go processUpdateStreamAssignment.
    /// </summary>
    public bool ProcessUpdateStreamAssignment(StreamAssignment sa)
    {
        if (!_assignments.TryGetValue(sa.StreamName, out var existing))
            return false;

        // Update the config while preserving consumers and other state
        var updated = new StreamAssignment
        {
            StreamName = sa.StreamName,
            Group = sa.Group,
            ConfigJson = sa.ConfigJson,
            Created = existing.Created,
            SyncSubject = existing.SyncSubject,
        };
        // Copy consumers from old to new
        foreach (var (name, ca) in existing.Consumers)
            updated.Consumers[name] = ca;

        _assignments[sa.StreamName] = updated;
        return true;
    }

    /// <summary>
    /// Removes a stream and all its consumers.
    /// Returns false if stream didn't exist. Returns true if removed.
    /// Go reference: jetstream_cluster.go processStreamRemoval.
    /// </summary>
    public bool ProcessStreamRemoval(string streamName)
    {
        if (!_assignments.ContainsKey(streamName))
            return false;

        RemoveStreamAssignment(streamName);
        return true;
    }

    /// <summary>
    /// Validates and processes a consumer assignment.
    /// Returns false if the parent stream does not exist, consumer name is empty, or if
    /// the assignment's version exceeds <see cref="CurrentVersion"/> (forward-compatibility skip).
    /// Version 0 is treated as version 1 for backward compatibility with pre-versioned entries.
    /// Go reference: jetstream_cluster.go:5300 processConsumerAssignment.
    /// </summary>
    public bool ProcessConsumerAssignment(ConsumerAssignment ca)
    {
        if (string.IsNullOrEmpty(ca.ConsumerName) || string.IsNullOrEmpty(ca.StreamName))
            return false;

        // Version 0 means pre-versioned (treat as current). Versions above CurrentVersion
        // are from a newer node and must be skipped to avoid corrupting cluster state.
        var effectiveVersion = ca.Version == 0 ? CurrentVersion : ca.Version;
        if (effectiveVersion > CurrentVersion)
        {
            Interlocked.Increment(ref _skippedUnsupportedEntries);
            return false;
        }

        if (!_assignments.ContainsKey(ca.StreamName))
            return false;

        AddConsumerAssignment(ca.StreamName, ca);
        return true;
    }

    /// <summary>
    /// Removes a consumer assignment.
    /// Returns false if stream or consumer doesn't exist.
    /// Go reference: jetstream_cluster.go processConsumerRemoval.
    /// </summary>
    public bool ProcessConsumerRemoval(string streamName, string consumerName)
    {
        if (!_assignments.TryGetValue(streamName, out var sa))
            return false;

        if (!sa.Consumers.ContainsKey(consumerName))
            return false;

        RemoveConsumerAssignment(streamName, consumerName);
        return true;
    }

    // ---------------------------------------------------------------
    // Monitor-facing mutation methods
    // Called by JetStreamClusterMonitor when processing committed RAFT entries.
    // ---------------------------------------------------------------

    /// <summary>
    /// Directly adds a stream assignment to the meta-group state.
    /// Used by the cluster monitor when processing RAFT entries.
    /// </summary>
    public void AddStreamAssignment(StreamAssignment sa)
    {
        _streams[sa.StreamName] = 0;
        _assignments[sa.StreamName] = sa;
    }

    /// <summary>
    /// Removes a stream assignment from the meta-group state.
    /// Used by the cluster monitor when processing RAFT entries.
    /// </summary>
    public void RemoveStreamAssignment(string streamName)
    {
        ApplyStreamDelete(streamName);
    }

    /// <summary>
    /// Adds a consumer assignment to a stream's assignment.
    /// Increments the total consumer count if the consumer is new.
    /// </summary>
    public void AddConsumerAssignment(string streamName, ConsumerAssignment ca)
    {
        if (_assignments.TryGetValue(streamName, out var sa))
        {
            var isNew = !sa.Consumers.ContainsKey(ca.ConsumerName);
            sa.Consumers[ca.ConsumerName] = ca;
            if (isNew)
                Interlocked.Increment(ref _totalConsumerCount);
        }
    }

    /// <summary>
    /// Removes a consumer assignment from a stream.
    /// </summary>
    public void RemoveConsumerAssignment(string streamName, string consumerName)
    {
        ApplyConsumerDelete(streamName, consumerName);
    }

    /// <summary>
    /// Replaces all assignments atomically (used for snapshot apply).
    /// Go reference: jetstream_cluster.go meta snapshot restore.
    /// </summary>
    public void ReplaceAllAssignments(Dictionary<string, StreamAssignment> newState)
    {
        _assignments.Clear();
        _streams.Clear();
        _totalConsumerCount = 0;

        foreach (var (name, sa) in newState)
        {
            _assignments[name] = sa;
            _streams[name] = 0;
            _totalConsumerCount += sa.Consumers.Count;
        }
    }

    // ---------------------------------------------------------------
    // ApplyEntry dispatch
    // Go reference: jetstream_cluster.go RAFT apply for meta group
    // ---------------------------------------------------------------

    /// <summary>
    /// Applies a committed RAFT entry to the meta-group state.
    /// Dispatches based on entry type prefix.
    /// Go reference: jetstream_cluster.go processStreamAssignment / processConsumerAssignment.
    /// </summary>
    public void ApplyEntry(MetaEntryType entryType, string name, string? streamName = null, RaftGroup? group = null)
    {
        switch (entryType)
        {
            case MetaEntryType.StreamCreate:
                ApplyStreamCreate(name, group ?? new RaftGroup { Name = name });
                break;
            case MetaEntryType.StreamDelete:
                ApplyStreamDelete(name);
                break;
            case MetaEntryType.ConsumerCreate:
                if (streamName is null)
                    throw new ArgumentNullException(nameof(streamName), "Stream name required for consumer operations.");
                ApplyConsumerCreate(streamName, name, group ?? new RaftGroup { Name = name });
                break;
            case MetaEntryType.ConsumerDelete:
                if (streamName is null)
                    throw new ArgumentNullException(nameof(streamName), "Stream name required for consumer operations.");
                ApplyConsumerDelete(streamName, name);
                break;
            case MetaEntryType.PeerAdd:
                ProcessAddPeer(name);
                break;
            case MetaEntryType.PeerRemove:
                ProcessRemovePeer(name);
                break;
            case MetaEntryType.Unknown:
            default:
                // Unknown or future entry types are counted and skipped.
                // This prevents an older node from crashing when it encounters a RAFT entry
                // produced by a newer cluster peer.
                // Go reference: jetstream_cluster.go — forward-compatibility fallback.
                Interlocked.Increment(ref _skippedUnsupportedEntries);
                break;
        }
    }

    // ---------------------------------------------------------------
    // Lookup
    // ---------------------------------------------------------------

    /// <summary>
    /// Returns the StreamAssignment for the given stream name, or null if not found.
    /// Go reference: jetstream_cluster.go streamAssignment lookup in meta leader.
    /// </summary>
    public StreamAssignment? GetStreamAssignment(string streamName)
        => _assignments.TryGetValue(streamName, out var assignment) ? assignment : null;

    /// <summary>
    /// Returns the ConsumerAssignment for the given stream and consumer, or null if not found.
    /// Go reference: jetstream_cluster.go consumerAssignment lookup.
    /// </summary>
    public ConsumerAssignment? GetConsumerAssignment(string streamName, string consumerName)
    {
        if (_assignments.TryGetValue(streamName, out var sa)
            && sa.Consumers.TryGetValue(consumerName, out var ca))
        {
            return ca;
        }

        return null;
    }

    /// <summary>
    /// Returns all current stream assignments.
    /// Go reference: jetstream_cluster.go meta leader assignment enumeration.
    /// </summary>
    public IReadOnlyCollection<StreamAssignment> GetAllAssignments()
        => _assignments.Values.ToArray();

    // ---------------------------------------------------------------
    // State
    // ---------------------------------------------------------------

    public MetaGroupState GetState()
    {
        return new MetaGroupState
        {
            Streams = _streams.Keys.OrderBy(x => x, StringComparer.Ordinal).ToArray(),
            ClusterSize = _nodes,
            LeaderId = $"meta-{_leaderIndex}",
            LeadershipVersion = _leadershipVersion,
            AssignmentCount = _assignments.Count,
            ConsumerCount = _totalConsumerCount,
        };
    }

    /// <summary>
    /// Fired when leadership changes. Argument is true when becoming leader, false when stepping down.
    /// Go reference: jetstream_cluster.go processLeaderChange callback.
    /// </summary>
    public event Action<bool>? OnLeaderChange;

    /// <summary>
    /// Processes a leadership change event.
    /// When stepping down: clears all inflight proposals.
    /// When becoming leader: fires OnLeaderChange event.
    /// Go reference: jetstream_cluster.go:7001-7074 processLeaderChange.
    /// </summary>
    public void ProcessLeaderChange(bool isLeader)
    {
        if (!isLeader)
        {
            // Stepping down — clear inflight
            ClearAllInflight();
        }

        OnLeaderChange?.Invoke(isLeader);
    }

    /// <summary>
    /// Steps down the current leader, rotating to the next node.
    /// Clears all inflight proposals on leader change via ProcessLeaderChange.
    /// Go reference: jetstream_cluster.go leader stepdown, clear inflight.
    /// </summary>
    public void StepDown()
    {
        _leaderIndex++;
        if (_leaderIndex > Math.Max(_nodes, 1))
            _leaderIndex = 1;

        Interlocked.Increment(ref _leadershipVersion);

        ProcessLeaderChange(isLeader: false);
    }

    // ---------------------------------------------------------------
    // Peer management
    // Go reference: jetstream_cluster.go:2290-2439 processAddPeer, processRemovePeer,
    //               removePeerFromStreamLocked, remapStreamAssignment.
    // ---------------------------------------------------------------

    /// <summary>
    /// Registers a peer as known to this meta-group.
    /// Go reference: jetstream_cluster.go peer tracking in jetStreamCluster.
    /// </summary>
    public void AddKnownPeer(string peerId)
    {
        lock (_knownPeers)
            _knownPeers.Add(peerId);
    }

    /// <summary>
    /// Removes a peer from the known-peers set.
    /// Go reference: jetstream_cluster.go peer removal tracking.
    /// </summary>
    public void RemoveKnownPeer(string peerId)
    {
        lock (_knownPeers)
            _knownPeers.Remove(peerId);
    }

    /// <summary>
    /// Returns a snapshot of the currently known peers.
    /// </summary>
    public IReadOnlyList<string> GetKnownPeers()
    {
        lock (_knownPeers)
            return [.. _knownPeers];
    }

    /// <summary>
    /// Processes the arrival of a new peer. Only the meta-group leader acts.
    /// Finds under-replicated streams (Group.Peers.Count &lt; desired replica count)
    /// and adds the new peer to their RaftGroup, triggering re-replication.
    /// Go reference: jetstream_cluster.go:2290 processAddPeer.
    /// </summary>
    public void ProcessAddPeer(string peerId)
    {
        // Always register the new peer.
        AddKnownPeer(peerId);

        // Only the meta-leader re-assigns streams.
        if (!IsLeader())
            return;

        foreach (var sa in _assignments.Values)
        {
            var group = sa.Group;
            if (group.Peers.Contains(peerId))
                continue;

            // Desired replicas is derived from the peer list size at creation time.
            // In this model, the Group records the desired size via its original Peers count.
            // An under-replicated group is one that is missing peers.
            // Go reference: jetstream_cluster.go:2284 sa.missingPeers() — len(Peers) < Config.Replicas.
            // Here, DesiredReplicas is stored on the group; if absent we skip (already at desired).
            if (!sa.Group.HasDesiredReplicas)
                continue;

            if (group.Peers.Count < sa.Group.DesiredReplicas)
            {
                // Add the new peer to restore the desired replica count.
                group.Peers.Add(peerId);
            }
        }
    }

    /// <summary>
    /// Processes the removal of a peer. Only the meta-group leader acts.
    /// Finds all streams that had the removed peer in their RaftGroup and
    /// triggers reassignment away from that peer.
    /// Go reference: jetstream_cluster.go:2342 processRemovePeer.
    /// </summary>
    public void ProcessRemovePeer(string peerId)
    {
        // Always remove from known set.
        RemoveKnownPeer(peerId);

        // Only the meta-leader re-assigns streams.
        if (!IsLeader())
            return;

        foreach (var sa in _assignments.Values)
        {
            if (sa.Group.Peers.Contains(peerId))
                RemovePeerFromStream(sa.StreamName, peerId);
        }
    }

    /// <summary>
    /// Removes a specific peer from a stream's RaftGroup and remaps to a replacement
    /// drawn from the known peers if possible.
    /// Returns true if a replacement peer was found; false if the peer list was merely shrunk.
    /// Go reference: jetstream_cluster.go:2403 removePeerFromStreamLocked.
    /// </summary>
    public bool RemovePeerFromStream(string streamName, string peerId)
    {
        if (!_assignments.TryGetValue(streamName, out var sa))
            return false;

        if (!sa.Group.Peers.Contains(peerId))
            return false;

        IReadOnlyList<string> available;
        lock (_knownPeers)
            available = [.. _knownPeers];

        return RemapStreamAssignment(sa, available, peerId);
    }

    /// <summary>
    /// Reassigns a stream's replica group to a new peer set, replacing the removed peer
    /// with one drawn from <paramref name="availablePeers"/>.
    /// Retains all current peers except <paramref name="removePeer"/>, then fills the
    /// vacancy from the available pool.
    /// Returns true when a replacement peer was placed; false if the group was merely shrunk.
    /// Go reference: jetstream_cluster.go:7077 remapStreamAssignment.
    /// </summary>
    public bool RemapStreamAssignment(StreamAssignment assignment, IReadOnlyList<string> availablePeers, string removePeer)
    {
        var group = assignment.Group;
        var currentPeers = group.Peers;

        // Peers to retain (all except the removed peer).
        var retain = currentPeers.Where(p => p != removePeer).ToList();

        // Candidates: available peers not already in the retained set and not the removed peer.
        var retainSet = new HashSet<string>(retain, StringComparer.Ordinal);
        var candidates = availablePeers
            .Where(p => !retainSet.Contains(p) && p != removePeer)
            .ToList();

        if (candidates.Count > 0)
        {
            // Pick the first available replacement.
            retain.Add(candidates[0]);
            currentPeers.Clear();
            foreach (var p in retain)
                currentPeers.Add(p);
            return true;
        }

        // No replacement available — just shrink the group if R>1.
        // Go reference: jetstream_cluster.go:7098-7110 R1 fallback, bare removal for R>1.
        if (currentPeers.Count > 1)
        {
            currentPeers.Remove(removePeer);
        }

        return false;
    }

    // ---------------------------------------------------------------
    // Internal apply methods
    // ---------------------------------------------------------------

    private void ApplyStreamCreate(string streamName, RaftGroup group)
    {
        _streams[streamName] = 0;

        _assignments.AddOrUpdate(
            streamName,
            name => new StreamAssignment
            {
                StreamName = name,
                Group = group,
                ConfigJson = "{}",
            },
            (_, existing) => existing);
    }

    private void ApplyStreamDelete(string streamName)
    {
        if (_assignments.TryRemove(streamName, out var removed))
        {
            // Decrement consumer count for all consumers in this stream
            Interlocked.Add(ref _totalConsumerCount, -removed.Consumers.Count);
        }

        _streams.TryRemove(streamName, out _);
    }

    private void ApplyConsumerCreate(string streamName, string consumerName, RaftGroup group)
    {
        if (_assignments.TryGetValue(streamName, out var streamAssignment))
        {
            var isNew = !streamAssignment.Consumers.ContainsKey(consumerName);
            streamAssignment.Consumers[consumerName] = new ConsumerAssignment
            {
                ConsumerName = consumerName,
                StreamName = streamName,
                Group = group,
            };

            if (isNew)
                Interlocked.Increment(ref _totalConsumerCount);
        }
    }

    private void ApplyConsumerDelete(string streamName, string consumerName)
    {
        if (_assignments.TryGetValue(streamName, out var streamAssignment))
        {
            if (streamAssignment.Consumers.Remove(consumerName))
                Interlocked.Decrement(ref _totalConsumerCount);
        }
    }
}

/// <summary>
/// Types of entries that can be proposed/applied in the meta group.
/// Go reference: jetstream_cluster.go entry type constants.
/// </summary>
public enum MetaEntryType
{
    StreamCreate,
    StreamDelete,
    ConsumerCreate,
    ConsumerDelete,
    /// <summary>
    /// A peer joined the cluster; triggers re-replication of under-replicated streams.
    /// Go reference: jetstream_cluster.go processAddPeer.
    /// </summary>
    PeerAdd,
    /// <summary>
    /// A peer left the cluster; triggers reassignment away from the removed peer.
    /// Go reference: jetstream_cluster.go processRemovePeer.
    /// </summary>
    PeerRemove,
    /// <summary>
    /// Sentinel value for an unrecognised entry type received from a newer cluster peer.
    /// Applying an Unknown entry increments <see cref="JetStreamMetaGroup.SkippedUnsupportedEntries"/>
    /// and is otherwise a no-op, ensuring forward compatibility without crashing.
    /// Go reference: jetstream_cluster.go — unknown entry type graceful handling.
    /// </summary>
    Unknown,
}

public sealed class MetaGroupState
{
    public IReadOnlyList<string> Streams { get; init; } = [];
    public int ClusterSize { get; init; }
    public string LeaderId { get; init; } = string.Empty;
    public long LeadershipVersion { get; init; }

    /// <summary>
    /// Number of stream assignments currently tracked by the meta group.
    /// </summary>
    public int AssignmentCount { get; init; }

    /// <summary>
    /// Total consumer count across all stream assignments.
    /// </summary>
    public int ConsumerCount { get; init; }
}

/// <summary>
/// Tracks an inflight stream or consumer proposal with ops counting.
/// OpsCount increments on duplicate proposals so that each proposer must
/// independently call Remove before the entry is cleared.
/// Go reference: jetstream_cluster.go inflight proposal tracking.
/// </summary>
public record InflightInfo(int OpsCount, bool Deleted, StreamAssignment? Assignment);
