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

using System.Collections.Concurrent;
using System.Threading.Channels;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;

namespace ZB.MOM.NatsNet.Server;

// ============================================================================
// RaftNode interface
// ============================================================================

/// <summary>
/// Primary interface for a NATS Consensus Group (NRG) Raft node.
/// Mirrors Go <c>RaftNode</c> interface in server/raft.go lines 40-92.
/// Replaces the stub declared in NatsServerTypes.cs.
/// </summary>
public interface IRaftNode
{
    // --- Proposal ---
    void Propose(byte[] entry);
    void ProposeMulti(IReadOnlyList<Entry> entries);
    void ForwardProposal(byte[] entry);

    // --- Snapshot ---
    void InstallSnapshot(byte[] snap, bool force);
    /// <remarks>Returns <see cref="IRaftNodeCheckpoint"/>; typed as object to keep the public interface free of internal types.</remarks>
    object CreateSnapshotCheckpoint(bool force);
    void SendSnapshot(byte[] snap);
    bool NeedSnapshot();

    // --- State queries ---
    (ulong Entries, ulong Bytes) Applied(ulong index);
    (ulong Entries, ulong Bytes) Processed(ulong index, ulong applied);
    RaftState State();
    (ulong Entries, ulong Bytes) Size();
    (ulong Index, ulong Commit, ulong Applied) Progress();
    bool Leader();
    DateTime? LeaderSince();
    bool Quorum();
    bool Current();
    bool Healthy();
    ulong Term();
    bool Leaderless();
    string GroupLeader();
    bool HadPreviousLeader();

    // --- Leadership / observer ---
    void StepDown(params string[] preferred);
    void SetObserver(bool isObserver);
    bool IsObserver();
    void Campaign();
    void CampaignImmediately();

    // --- Identity ---
    string ID();
    string Group();

    // --- Peer management ---
    IReadOnlyList<Peer> Peers();
    void ProposeKnownPeers(IReadOnlyList<string> knownPeers);
    void UpdateKnownPeers(IReadOnlyList<string> knownPeers);
    void ProposeAddPeer(string peer);
    void ProposeRemovePeer(string peer);
    bool MembershipChangeInProgress();
    void AdjustClusterSize(int csz);
    void AdjustBootClusterSize(int csz);
    int ClusterSize();

    // --- Apply queue ---
    IpQueue<CommittedEntry> ApplyQ();
    void PauseApply();
    void ResumeApply();
    bool DrainAndReplaySnapshot();

    // --- Channels / lifecycle ---
    ChannelReader<bool> LeadChangeC();
    ChannelReader<bool> QuitC();
    DateTime Created();
    void Stop();
    void WaitForStop();
    void Delete();
    bool IsDeleted();
    void RecreateInternalSubs();
    bool IsSystemAccount();
    string GetTrafficAccountName();

    // Batch 30 mapped methods (server/raft.go)
    bool CheckAccountNRGStatus();
    Exception? RecreateInternalSubsLocked();
    bool OutOfResources();
    void PauseApplyLocked();

    // Group C
    void Shutdown();
    string NewCatchupInbox();
    string NewInbox();
    object Subscribe(string subject, Action<byte[]?>? handler = null);
    void Unsubscribe(object subscription);
    Exception? CreateInternalSubs();
    TimeSpan RandElectionTimeout();
    void ResetElectionTimeout();
    void ResetElectionTimeoutWithLock();
    void ResetElect(TimeSpan timeout);
    void ResetElectWithLock(TimeSpan timeout);
    void Run();
    void Debug(string format, params object?[] args);
    void Warn(string format, params object?[] args);
    void Error(string format, params object?[] args);
    DateTime ElectTimer();
    void SetObserverInternal(bool isObserver);
    void SetObserverLocked(bool isObserver);
    void ProcessAppendEntries(AppendEntry appendEntry);
    void RunAsFollower();
    void SendSnapshotToFollower(string peer, byte[] snapshot, ulong term = 0, ulong index = 0);
    CatchupState CatchupFollower(string peer, ulong term, ulong index);
    Entry? LoadEntry(ulong index);
    CommittedEntry? ApplyCommit(ulong commitIndex);
    bool TryCommit(ulong commitIndex);
    void TrackResponse(AppendEntryResponse response);
    void AdjustClusterSizeAndQuorum();
    void TrackPeer(string peer, ulong index);
    void RunAsCandidate();
    void HandleAppendEntry(AppendEntry appendEntry);
    void CancelCatchup();
    bool CatchupStalled(TimeSpan? threshold = null);
    CatchupState CreateCatchup();
    bool SendCatchupSignal();
    void CancelCatchupSignal();
    void TruncateWAL(ulong index);
    void ResetWAL();
    void UpdateLeader(string leaderId);

    // Group D
    CommittedEntry NewCommittedEntry(ulong index, IReadOnlyList<Entry>? entries = null);
    Entry NewEntry(EntryType type, byte[]? data = null);
    AppendEntry NewAppendEntry(string leader, ulong term, ulong commit, ulong prevTerm, ulong prevIndex, IReadOnlyList<Entry>? entries = null);
    ProposedEntry NewProposedEntry(Entry? entry = null, string? reply = null);
    AppendEntry DecodeAppendEntry(byte[] buffer);
    AppendEntryResponse NewAppendEntryResponse(ulong term, ulong index, string peer, string reply, bool success);
    AppendEntryResponse DecodeAppendEntryResponse(byte[] buffer);

    // Group D/E
    void HandleForwardedRemovePeerProposal(string peer);
    void HandleForwardedProposal(byte[] entry);
    void AddPeer(string peer);
    void RemovePeer(string peer);
    void SendMembershipChange(EntryType changeType, string peer);
    void RunAsLeader();
    bool LostQuorum();
    bool LostQuorumLocked();
    bool NotActive();
    AppendEntry? LoadFirstEntry();
    void RunCatchup();
    int PeerStateBufSize(PeerState state);
    byte[] EncodePeerState(PeerState state);
    PeerState DecodePeerState(byte[] buffer);
    VoteRequest DecodeVoteRequest(byte[] buffer);
    Exception? WritePeerStateStatic(string storeDir, PeerState state);
    (PeerState? State, Exception? Error) ReadPeerState(string storeDir);
    Exception? WriteTermVoteStatic(string storeDir, ulong term, string vote);
    VoteResponse DecodeVoteResponse(byte[] buffer);
    void ProcessAppendEntry(AppendEntry appendEntry);
    void ResetInitializing();
    void ProcessPeerState(Entry entry);
    void ProcessAppendEntryResponse(AppendEntryResponse response);
    void HandleAppendEntryResponse(AppendEntryResponse response);
    AppendEntry BuildAppendEntry(string peer);
    bool StoreToWAL(AppendEntry appendEntry);
    void SendAppendEntry(AppendEntry appendEntry, string? peer = null);
    void SendAppendEntryLocked(string peer, AppendEntry appendEntry);
    void CachePendingEntry(AppendEntry appendEntry, ulong? index = null);
    IReadOnlyList<string> PeerNames();
    PeerState CurrentPeerState();
    PeerState CurrentPeerStateLocked();
    void SendPeerState();
    void SendHeartbeat();
    Exception? WritePeerState();
    (ulong Term, string Vote, Exception? Error) ReadTermVote();
    void SetWriteErrLocked(Exception? error);
    bool IsClosed();
    void SetWriteErr(Exception? error);
    Exception? WriteTermVote();
    void HandleVoteResponse(VoteResponse response);
    bool ProcessVoteRequest(VoteRequest request);
    void HandleVoteRequest(VoteRequest request);
    void RequestVote();
    void SendRPC(string subject, byte[] payload);
    void SendReply(string subject, byte[] payload);
    bool WonElection();
    int QuorumNeeded();
    void UpdateLeadChange(bool isLeader);
    void SwitchState(RaftState newState);
    void SwitchToFollower(string newLeader = "");
    void SwitchToFollowerLocked(string newLeader = "");
    void SwitchToCandidate();
    void SwitchToLeader();
}

// ============================================================================
// RaftNodeCheckpoint interface
// ============================================================================

/// <summary>
/// Allows asynchronous snapshot installation from a checkpoint.
/// Mirrors Go <c>RaftNodeCheckpoint</c> interface in server/raft.go lines 98-103.
/// Internal because it references internal types (AppendEntry).
/// </summary>
internal interface IRaftNodeCheckpoint
{
    byte[] LoadLastSnapshot();
    IEnumerable<(AppendEntry Entry, Exception? Error)> AppendEntriesSeq();
    void Abort();
    ulong InstallSnapshot(byte[] data);
}

// ============================================================================
// IWal interface
// ============================================================================

/// <summary>
/// Write-ahead log abstraction used by the Raft implementation.
/// Mirrors Go <c>WAL</c> interface in server/raft.go lines 105-118.
/// </summary>
internal interface IWal
{
    StorageType Type();
    (ulong Seq, long TimeStamp) StoreMsg(string subj, byte[]? hdr, byte[] msg, long ttl);
    StoreMsg? LoadMsg(ulong index, StoreMsg? sm);
    bool RemoveMsg(ulong index);
    ulong Compact(ulong index);
    ulong Purge();
    ulong PurgeEx(string subject, ulong seq, ulong keep);
    void Truncate(ulong seq);
    StreamState State();
    void FastState(ref StreamState state);
    void Stop();
    void Delete(bool inline);
}

// ============================================================================
// Peer
// ============================================================================

/// <summary>
/// Represents a peer in a Raft group.
/// Mirrors Go <c>Peer</c> struct in server/raft.go lines 120-125.
/// </summary>
public sealed class Peer
{
    public string   Id      { get; set; } = string.Empty;
    public bool     Current { get; set; }
    public DateTime Last    { get; set; }
    public ulong    Lag     { get; set; }
}

// ============================================================================
// RaftState enum
// ============================================================================

/// <summary>
/// Allowable states for a NATS Consensus Group node.
/// Mirrors Go <c>RaftState</c> iota in server/raft.go lines 128-135.
/// </summary>
public enum RaftState : byte
{
    Follower  = 0,
    Leader    = 1,
    Candidate = 2,
    Closed    = 3,
}

// ============================================================================
// RaftConfig
// ============================================================================

/// <summary>
/// Configuration for creating a Raft group.
/// Mirrors Go <c>RaftConfig</c> struct in server/raft.go lines 301-317.
/// </summary>
public sealed class RaftConfig
{
    public string  Name       { get; set; } = string.Empty;
    public string  Store      { get; set; } = string.Empty;
    /// <summary>WAL store — typed as object to keep public API free of internal IWal.</summary>
    public object? Log        { get; set; }
    public bool    Track      { get; set; }
    public bool    Observer   { get; set; }
    /// <summary>
    /// Must be set for a Raft group that's recovering after a restart, or if first
    /// seen after a catchup from another server.
    /// </summary>
    public bool    Recovering { get; set; }
    /// <summary>
    /// Identifies the Raft peer set is being scaled up; prevents an empty-log node
    /// from becoming leader prematurely.
    /// </summary>
    public bool    ScaleUp    { get; set; }
}

// ============================================================================
// Internal Raft state types
// ============================================================================

/// <summary>
/// Main Raft node implementation.
/// Mirrors Go <c>raft</c> struct in server/raft.go lines 151-251.
/// All algorithm methods are stubbed — full implementation is session 20+.
/// </summary>
internal sealed partial class Raft : IRaftNode
{
    // Identity / location
    internal DateTime Created_    { get; set; }
    internal string   AccName     { get; set; } = string.Empty;
    internal string   GroupName   { get; set; } = string.Empty;
    internal string   StoreDir    { get; set; } = string.Empty;
    internal string   Id          { get; set; } = string.Empty;

    // WAL (IWal is internal; store as object here to keep the field in the class without accessibility issues)
    internal object?     Wal       { get; set; }
    internal StorageType WalType   { get; set; }
    internal ulong       WalBytes  { get; set; }
    internal Exception?  WriteErr  { get; set; }

    // Atomic state
    internal int    StateValue    { get; set; }   // RaftState (use Interlocked)
    internal long   LeaderStateV  { get; set; }   // 1 = in complete leader state
    internal string SnapFile      { get; set; } = string.Empty;

    // Cluster
    internal int    Csz           { get; set; }   // cluster size
    internal int    Qn            { get; set; }   // quorum node count
    internal Dictionary<string, Lps>  Peers_     { get; set; } = new();

    // Tracking removed peers
    internal Dictionary<string, DateTime>              Removed  { get; set; } = new();
    internal Dictionary<ulong, Dictionary<string, bool>> Acks   { get; set; } = new();
    internal Dictionary<ulong, AppendEntry>            Pae      { get; set; } = new();

    // Timers / activity
    internal System.Threading.Timer? Elect   { get; set; }
    internal DateTime Active                 { get; set; }
    internal DateTime Llqrt                  { get; set; }
    internal DateTime Lsut                   { get; set; }

    // Term / index tracking
    internal ulong Term_      { get; set; }
    internal ulong PTerm      { get; set; }
    internal ulong PIndex     { get; set; }
    internal ulong Commit     { get; set; }
    internal ulong Processed_ { get; set; }
    internal ulong Applied_   { get; set; }
    internal ulong PApplied   { get; set; }

    internal ulong MembChangeIndex { get; set; }
    internal ulong Aflr            { get; set; }

    internal string LeaderId { get; set; } = string.Empty;
    internal string Vote     { get; set; } = string.Empty;

    // Server references (object to avoid circular deps)
    internal object?  Server_    { get; set; }
    internal object?  Client_    { get; set; }
    internal object?  JetStream_ { get; set; }

    // Atomic booleans — must be fields (not auto-properties) for Interlocked
    internal long HasLeaderV;
    internal long PLeaderV;
    private  long _isSysAccV;

    // NATS subjects
    internal string PSubj   { get; set; } = string.Empty;
    internal string RpSubj  { get; set; } = string.Empty;
    internal string VSubj   { get; set; } = string.Empty;
    internal string VReply  { get; set; } = string.Empty;
    internal string ASubj   { get; set; } = string.Empty;
    internal string AReply  { get; set; } = string.Empty;

    // Queues (object placeholder — IpQueue<T> from session 02)
    internal object?  SendQ   { get; set; }
    internal object?  AeSub   { get; set; }

    // Write buffers
    internal byte[] Wtv { get; set; } = [];
    internal byte[] Wps { get; set; } = [];

    // Catchup / progress
    internal CatchupState?                               Catchup  { get; set; }
    internal Dictionary<string, IpQueue<ulong>>?         Progress_ { get; set; }

    internal ulong HCommit { get; set; }

    // Queues (typed as object to avoid pulling IpQueue<T> directly into the struct boundary)
    internal IpQueue<ProposedEntry>?      PropQ  { get; set; }
    internal IpQueue<AppendEntry>?        EntryQ { get; set; }
    internal IpQueue<AppendEntryResponse>? RespQ { get; set; }
    internal IpQueue<CommittedEntry>?     ApplyQ_ { get; set; }
    internal IpQueue<VoteRequest>?        Reqs   { get; set; }
    internal IpQueue<VoteResponse>?       Votes_ { get; set; }

    internal Channel<bool>? LeadC { get; set; }
    internal Channel<bool>? Quit  { get; set; }

    // Flags
    internal bool Lxfer        { get; set; }
    internal bool HcBehind     { get; set; }
    internal bool MaybeLeader  { get; set; }
    internal bool Track        { get; set; }
    internal bool DebugEnabled { get; set; }
    internal bool Paused       { get; set; }
    internal bool Observer_    { get; set; }
    internal bool Initializing { get; set; }
    internal bool ScaleUp_     { get; set; }
    internal bool Deleted_     { get; set; }
    internal bool Snapshotting { get; set; }

    // Lock
    private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);

    // -----------------------------------------------------------------------
    // IRaftNode — stub implementations
    // -----------------------------------------------------------------------
    public void Propose(byte[] entry)
    {
        ArgumentNullException.ThrowIfNull(entry);

        _lock.EnterWriteLock();
        try
        {
            PropQ ??= new IpQueue<ProposedEntry>($"{GroupName}-propose");
            var pe = new ProposedEntry
            {
                Entry = new Entry { Type = EntryType.EntryNormal, Data = [.. entry] },
            };
            PropQ.Push(pe);
            Active = DateTime.UtcNow;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void ProposeMulti(IReadOnlyList<Entry> entries)
    {
        ArgumentNullException.ThrowIfNull(entries);
        foreach (var entry in entries)
        {
            if (entry == null)
                continue;

            Propose(entry.Data);
        }
    }

    public void ForwardProposal(byte[] entry) => Propose(entry);

    public void InstallSnapshot(byte[] snap, bool force)
    {
        ArgumentNullException.ThrowIfNull(snap);

        _lock.EnterWriteLock();
        try
        {
            if (Snapshotting && !force)
                return;

            Snapshotting = true;
            Wps = [.. snap];
            if (force)
                Applied_ = Commit;
            Snapshotting = false;
            Active = DateTime.UtcNow;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public object CreateSnapshotCheckpoint(bool force) => new Checkpoint
    {
        Node = this,
        Term = Term_,
        Applied = Applied_,
        PApplied = PApplied,
        SnapFile = force ? string.Empty : SnapFile,
        PeerState = [.. Wps],
    };

    public void SendSnapshot(byte[] snap) => InstallSnapshot(snap, force: false);

    public bool NeedSnapshot()
    {
        _lock.EnterReadLock();
        try
        {
            return Snapshotting || PApplied > Applied_;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public (ulong, ulong) Applied(ulong index)
    {
        _lock.EnterReadLock();
        try
        {
            var entries = Applied_ >= index ? Applied_ - index : 0;
            return (entries, WalBytes);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public (ulong, ulong) Processed(ulong index, ulong applied)
    {
        _lock.EnterWriteLock();
        try
        {
            if (index > Processed_)
                Processed_ = index;
            if (applied > Applied_)
                Applied_ = applied;
            return (Processed_, WalBytes);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }
    public RaftState State()                                              => (RaftState)StateValue;
    public (ulong, ulong) Size()
    {
        _lock.EnterReadLock();
        try
        {
            return (Processed_, WalBytes);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public (ulong, ulong, ulong) Progress()
    {
        _lock.EnterReadLock();
        try
        {
            return (PIndex, Commit, Applied_);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public bool Leader() => State() == RaftState.Leader;

    public DateTime? LeaderSince()
    {
        _lock.EnterReadLock();
        try
        {
            return Leader() ? (Lsut == default ? Active : Lsut) : null;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public bool Quorum()
    {
        _lock.EnterReadLock();
        try
        {
            var clusterSize = ClusterSize();
            if (clusterSize <= 1)
                return true;

            var required = Qn > 0 ? Qn : (clusterSize / 2) + 1;
            var available = 1; // self
            var now = DateTime.UtcNow;
            foreach (var peer in Peers_.Values)
            {
                if (peer.Kp || now - peer.Ts <= TimeSpan.FromSeconds(30))
                    available++;
            }

            return available >= required;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public bool Current()
    {
        _lock.EnterReadLock();
        try
        {
            return !Deleted_ && !Leaderless();
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public bool Healthy() => Current() && Quorum();
    public ulong Term()                                                   => Term_;
    public bool Leaderless()                                              => string.IsNullOrEmpty(LeaderId) && Interlocked.Read(ref HasLeaderV) == 0;
    public string GroupLeader()                                           => Leader() ? Id : LeaderId;
    public bool HadPreviousLeader()                                       => Interlocked.Read(ref PLeaderV) != 0 || !string.IsNullOrEmpty(LeaderId);

    public void StepDown(params string[] preferred)
    {
        _lock.EnterWriteLock();
        try
        {
            StateValue = (int)RaftState.Follower;
            Interlocked.Exchange(ref HasLeaderV, 0);
            Interlocked.Exchange(ref PLeaderV, 1);
            Lxfer = true;
            Lsut = DateTime.UtcNow;
            if (preferred is { Length: > 0 })
                Vote = preferred[0];
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void SetObserver(bool isObserver)
    {
        _lock.EnterWriteLock();
        try
        {
            Observer_ = isObserver;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public bool IsObserver() => Observer_;

    public void Campaign()
    {
        _lock.EnterWriteLock();
        try
        {
            if (Deleted_)
                return;

            StateValue = (int)RaftState.Candidate;
            Active = DateTime.UtcNow;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void CampaignImmediately() => Campaign();
    public string ID()                                                    => Id;
    public string Group()                                                 => GroupName;
    public IReadOnlyList<Peer> Peers()
    {
        _lock.EnterReadLock();
        try
        {
            var peers = new List<Peer>(Peers_.Count);
            foreach (var (id, state) in Peers_)
            {
                peers.Add(new Peer
                {
                    Id = id,
                    Current = state.Kp,
                    Last = state.Ts,
                    Lag = PIndex >= state.Li ? PIndex - state.Li : 0,
                });
            }

            return peers;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public void ProposeKnownPeers(IReadOnlyList<string> knownPeers)
    {
        ArgumentNullException.ThrowIfNull(knownPeers);

        _lock.EnterWriteLock();
        try
        {
            var now = DateTime.UtcNow;
            foreach (var lps in Peers_.Values)
                lps.Kp = false;

            foreach (var peer in knownPeers)
            {
                if (string.IsNullOrWhiteSpace(peer))
                    continue;

                if (!Peers_.TryGetValue(peer, out var lps))
                {
                    lps = new Lps();
                    Peers_[peer] = lps;
                }

                lps.Kp = true;
                lps.Ts = now;
            }

            Csz = Math.Max(knownPeers.Count + 1, 1);
            Qn = (Csz / 2) + 1;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void UpdateKnownPeers(IReadOnlyList<string> knownPeers) => ProposeKnownPeers(knownPeers);

    public void ProposeAddPeer(string peer)
    {
        if (string.IsNullOrWhiteSpace(peer))
            return;

        _lock.EnterWriteLock();
        try
        {
            if (!Peers_.TryGetValue(peer, out var lps))
            {
                lps = new Lps();
                Peers_[peer] = lps;
            }

            lps.Kp = true;
            lps.Ts = DateTime.UtcNow;
            MembChangeIndex = PIndex + 1;
            Csz = Math.Max(Peers_.Count + 1, 1);
            Qn = (Csz / 2) + 1;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void ProposeRemovePeer(string peer)
    {
        if (string.IsNullOrWhiteSpace(peer))
            return;

        _lock.EnterWriteLock();
        try
        {
            Peers_.Remove(peer);
            Removed[peer] = DateTime.UtcNow;
            MembChangeIndex = PIndex + 1;
            Csz = Math.Max(Peers_.Count + 1, 1);
            Qn = (Csz / 2) + 1;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public bool MembershipChangeInProgress()
    {
        _lock.EnterReadLock();
        try
        {
            return MembChangeIndex != 0 && MembChangeIndex > Applied_;
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    public void AdjustClusterSize(int csz)
    {
        _lock.EnterWriteLock();
        try
        {
            Csz = Math.Max(csz, 1);
            Qn = (Csz / 2) + 1;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void AdjustBootClusterSize(int csz) => AdjustClusterSize(csz);

    public int ClusterSize()
    {
        _lock.EnterReadLock();
        try
        {
            return Csz > 0 ? Csz : Math.Max(Peers_.Count + 1, 1);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }
    public IpQueue<CommittedEntry> ApplyQ()                               => ApplyQ_ ?? throw new InvalidOperationException("Apply queue not initialized");
    public void PauseApply()
    {
        _lock.EnterWriteLock();
        try
        {
            PauseApplyLocked();
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }
    public void ResumeApply()                                             => Paused = false;

    public bool DrainAndReplaySnapshot()
    {
        _lock.EnterWriteLock();
        try
        {
            var canReplay = !Snapshotting;
            if (canReplay)
            {
                HcBehind = false;
            }

            return canReplay;
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }
    public ChannelReader<bool> LeadChangeC()                              => LeadC?.Reader ?? throw new InvalidOperationException("Lead channel not initialized");
    public ChannelReader<bool> QuitC()                                    => Quit?.Reader ?? throw new InvalidOperationException("Quit channel not initialized");
    public DateTime Created()                                             => Created_;
    public void Stop()
    {
        _lock.EnterWriteLock();
        try
        {
            StateValue = (int)RaftState.Closed;
            Elect?.Dispose();
            Elect = null;
            Quit ??= Channel.CreateUnbounded<bool>();
            Quit.Writer.TryWrite(true);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void WaitForStop()
    {
        var q = Quit;
        if (q == null)
            return;

        if (q.Reader.TryRead(out _))
            return;

        q.Reader.WaitToReadAsync().AsTask().Wait(TimeSpan.FromSeconds(1));
    }

    public void Delete()
    {
        Deleted_ = true;
        Stop();
    }
    public bool IsDeleted()                                               => Deleted_;
    public void RecreateInternalSubs()
    {
        _lock.EnterWriteLock();
        try
        {
            var error = RecreateInternalSubsLocked();
            if (error is not null)
            {
                throw error;
            }
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }
    public bool IsSystemAccount()                                         => Interlocked.Read(ref _isSysAccV) != 0;
    public string GetTrafficAccountName()
        => IsSystemAccount() ? "$SYS" : (string.IsNullOrEmpty(AccName) ? "$G" : AccName);
}

// ============================================================================
// ProposedEntry
// ============================================================================

/// <summary>
/// An entry that has been proposed to the leader, with an optional reply subject.
/// Mirrors Go <c>proposedEntry</c> struct in server/raft.go lines 253-256.
/// </summary>
public sealed class ProposedEntry
{
    public Entry?  Entry { get; set; }
    public string  Reply { get; set; } = string.Empty;

    public void ReturnToPool()
    {
        Entry = null;
        Reply = string.Empty;
    }
}

// ============================================================================
// CatchupState
// ============================================================================

/// <summary>
/// Tracks the state of a follower catch-up operation.
/// Mirrors Go <c>catchupState</c> struct in server/raft.go lines 259-268.
/// </summary>
public sealed class CatchupState
{
    /// <summary>Subscription that catchup messages arrive on (object to avoid session dep).</summary>
    public object?   Sub     { get; set; }
    public ulong     CTerm   { get; set; }
    public ulong     CIndex  { get; set; }
    public ulong     PTerm   { get; set; }
    public ulong     PIndex  { get; set; }
    public DateTime  Active  { get; set; }
    public bool      Signal  { get; set; }
}

// ============================================================================
// Lps — leader peer state
// ============================================================================

/// <summary>
/// Per-peer state tracked by the leader: last timestamp and last index replicated.
/// Mirrors Go <c>lps</c> struct in server/raft.go lines 271-275.
/// </summary>
internal sealed class Lps
{
    /// <summary>Last timestamp.</summary>
    public DateTime Ts { get; set; }
    /// <summary>Last index replicated.</summary>
    public ulong    Li { get; set; }
    /// <summary>Whether this is a known peer.</summary>
    public bool     Kp { get; set; }
}

// ============================================================================
// Snapshot (internal)
// ============================================================================

/// <summary>
/// An encoded Raft snapshot (on-disk format).
/// Mirrors Go <c>snapshot</c> struct in server/raft.go lines 1243-1248.
/// </summary>
internal sealed class Snapshot
{
    public ulong  LastTerm  { get; set; }
    public ulong  LastIndex { get; set; }
    public byte[] PeerState { get; set; } = [];
    public byte[] Data      { get; set; } = [];
}

// ============================================================================
// Checkpoint (internal)
// ============================================================================

/// <summary>
/// Checkpoint for asynchronous snapshot installation.
/// Mirrors Go <c>checkpoint</c> struct in server/raft.go lines 1414-1421.
/// Implements <see cref="IRaftNodeCheckpoint"/>.
/// </summary>
internal sealed class Checkpoint : IRaftNodeCheckpoint
{
    public Raft?   Node      { get; set; }
    public ulong   Term      { get; set; }
    public ulong   Applied   { get; set; }
    public ulong   PApplied  { get; set; }
    public string  SnapFile  { get; set; } = string.Empty;
    public byte[]  PeerState { get; set; } = [];

    public byte[] LoadLastSnapshot()
    {
        if (string.IsNullOrWhiteSpace(SnapFile))
            return [];

        try
        {
            return File.Exists(SnapFile) ? File.ReadAllBytes(SnapFile) : [];
        }
        catch
        {
            return [];
        }
    }

    public IEnumerable<(AppendEntry Entry, Exception? Error)> AppendEntriesSeq()
    {
        if (Node == null)
            yield break;

        var entry = new AppendEntry
        {
            Leader = Node.Id,
            TermV = Term,
            Commit = Applied,
            PTerm = Node.PTerm,
            PIndex = PApplied,
            Reply = Node.AReply,
        };

        yield return (entry, null);
    }

    public void Abort()
    {
        if (string.IsNullOrWhiteSpace(SnapFile))
            return;

        try
        {
            if (File.Exists(SnapFile))
                File.Delete(SnapFile);
        }
        catch
        {
            // Ignore cleanup failures for aborted checkpoints.
        }
    }

    public ulong InstallSnapshot(byte[] data)
    {
        ArgumentNullException.ThrowIfNull(data);

        if (string.IsNullOrWhiteSpace(SnapFile))
            SnapFile = Path.Combine(Path.GetTempPath(), $"raft-snapshot-{Guid.NewGuid():N}.bin");

        File.WriteAllBytes(SnapFile, data);
        Node?.InstallSnapshot(data, force: true);
        return (ulong)data.LongLength;
    }
}

// ============================================================================
// CommittedEntry
// ============================================================================

/// <summary>
/// A committed Raft entry passed up to the JetStream layer for application.
/// Mirrors Go <c>CommittedEntry</c> struct in server/raft.go lines 2506-2509.
/// </summary>
public sealed class CommittedEntry
{
    public ulong         Index   { get; set; }
    public List<Entry>   Entries { get; set; } = new();

    public void ReturnToPool()
    {
        Index = 0;
        Entries.Clear();
    }
}

// ============================================================================
// AppendEntry (internal)
// ============================================================================

/// <summary>
/// The main struct used to sync Raft peers.
/// Mirrors Go <c>appendEntry</c> struct in server/raft.go lines 2557-2568.
/// </summary>
public sealed class AppendEntry
{
    public string      Leader  { get; set; } = string.Empty;
    public ulong       TermV   { get; set; }
    public ulong       Commit  { get; set; }
    public ulong       PTerm   { get; set; }
    public ulong       PIndex  { get; set; }
    public List<Entry> Entries { get; set; } = new();
    /// <summary>Highest term for catchups (if 0, use Term).</summary>
    public ulong       LTerm   { get; set; }
    public string      Reply   { get; set; } = string.Empty;
    /// <summary>Subscription the append entry arrived on (object to avoid session dep).</summary>
    public object?     Sub     { get; set; }
    public byte[]?     Buf     { get; set; }

    public void ReturnToPool()
    {
        Leader = string.Empty;
        TermV = 0;
        Commit = 0;
        PTerm = 0;
        PIndex = 0;
        Entries.Clear();
        LTerm = 0;
        Reply = string.Empty;
        Sub = null;
        Buf = null;
    }
}

// ============================================================================
// EntryType enum
// ============================================================================

/// <summary>
/// Type of a Raft log entry.
/// Mirrors Go <c>EntryType</c> iota in server/raft.go lines 2605-2619.
/// </summary>
public enum EntryType : byte
{
    EntryNormal         = 0,
    EntryOldSnapshot    = 1,
    EntryPeerState      = 2,
    EntryAddPeer        = 3,
    EntryRemovePeer     = 4,
    EntryLeaderTransfer = 5,
    EntrySnapshot       = 6,
    /// <summary>Internal signal type — not transmitted between peers or stored in the log.</summary>
    EntryCatchup        = 7,
}

// ============================================================================
// Entry
// ============================================================================

/// <summary>
/// A single Raft log entry (type + opaque payload).
/// Mirrors Go <c>Entry</c> struct in server/raft.go lines 2641-2643.
/// </summary>
public sealed class Entry
{
    public EntryType Type { get; set; }
    public byte[]    Data { get; set; } = [];

    /// <summary>
    /// Returns true if this entry changes group membership.
    /// Mirrors Go <c>Entry.ChangesMembership</c>.
    /// </summary>
    public bool ChangesMembership()
        => Type == EntryType.EntryAddPeer || Type == EntryType.EntryRemovePeer;
}

// ============================================================================
// AppendEntryResponse (internal)
// ============================================================================

/// <summary>
/// Response sent by a follower after receiving an append-entry RPC.
/// Mirrors Go <c>appendEntryResponse</c> struct in server/raft.go lines 2760-2766.
/// </summary>
public sealed class AppendEntryResponse
{
    public ulong  TermV   { get; set; }
    public ulong  Index   { get; set; }
    public string Peer    { get; set; } = string.Empty;
    public string Reply   { get; set; } = string.Empty;
    public bool   Success { get; set; }

    public byte[] Encode() =>
        System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(this);
}

// ============================================================================
// PeerState (internal)
// ============================================================================

/// <summary>
/// Encoded peer state attached to snapshots and peer-state entries.
/// Mirrors Go <c>peerState</c> struct in server/raft.go lines 4470-4474.
/// </summary>
public sealed class PeerState
{
    public List<string> KnownPeers  { get; set; } = new();
    public int          ClusterSize { get; set; }
    /// <summary>Extension / domain state (opaque ushort in Go).</summary>
    public ushort       DomainExt   { get; set; }
}

// ============================================================================
// VoteRequest (internal)
// ============================================================================

/// <summary>
/// A Raft vote request sent during leader election.
/// Mirrors Go <c>voteRequest</c> struct in server/raft.go lines 4549-4556.
/// </summary>
public sealed class VoteRequest
{
    public ulong  TermV     { get; set; }
    public ulong  LastTerm  { get; set; }
    public ulong  LastIndex { get; set; }
    public string Candidate { get; set; } = string.Empty;
    /// <summary>Internal use — reply subject.</summary>
    public string Reply     { get; set; } = string.Empty;

    public byte[] Encode() =>
        System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(this);
}

// ============================================================================
// VoteResponse (internal)
// ============================================================================

/// <summary>
/// A response to a <see cref="VoteRequest"/>.
/// Mirrors Go <c>voteResponse</c> struct in server/raft.go lines 4730-4735.
/// </summary>
public sealed class VoteResponse
{
    public ulong  TermV   { get; set; }
    public string Peer    { get; set; } = string.Empty;
    public bool   Granted { get; set; }
    /// <summary>Whether this peer's log is empty.</summary>
    public bool   Empty   { get; set; }

    public byte[] Encode() =>
        System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(this);
}
