using System.Text.Json.Serialization;

namespace NATS.Server.JetStream.Cluster;

/// <summary>
/// RAFT group describing which peers own a replicated asset (stream or consumer).
/// Go reference: jetstream_cluster.go:154-163 raftGroup struct.
/// </summary>
public sealed class RaftGroup
{
    public required string Name { get; init; }
    public List<string> Peers { get; init; } = [];
    public string StorageType { get; set; } = "file";
    public string Cluster { get; set; } = string.Empty;
    public string Preferred { get; set; } = string.Empty;

    /// <summary>
    /// Optional desired replica count for this group.
    /// When set, ProcessAddPeer uses this to detect under-replication.
    /// Go reference: jetstream_cluster.go:2284 sa.missingPeers() — len(Peers) &lt; Config.Replicas.
    /// </summary>
    public int DesiredReplicas { get; set; }

    /// <summary>
    /// True when <see cref="DesiredReplicas"/> has been explicitly set (non-zero).
    /// </summary>
    public bool HasDesiredReplicas => DesiredReplicas > 0;

    public int QuorumSize => (Peers.Count / 2) + 1;
    public bool HasQuorum(int ackCount) => ackCount >= QuorumSize;

    /// <summary>
    /// True when the group has fewer peers than the desired replica count.
    /// Go reference: jetstream_cluster.go missingPeers — len(rg.Peers) &lt; DesiredReplicas.
    /// </summary>
    public bool IsUnderReplicated => HasDesiredReplicas && Peers.Count < DesiredReplicas;

    /// <summary>
    /// True when the group has more peers than the desired replica count.
    /// Go reference: jetstream_cluster.go extraPeers — len(rg.Peers) &gt; DesiredReplicas.
    /// </summary>
    public bool IsOverReplicated => HasDesiredReplicas && Peers.Count > DesiredReplicas;

    /// <summary>
    /// Returns true if the given peerId is a member of this group (case-sensitive).
    /// Go reference: jetstream_cluster.go isMember helper.
    /// </summary>
    public bool IsMember(string peerId) => Peers.Contains(peerId, StringComparer.Ordinal);

    /// <summary>
    /// Sets the preferred leader peer for this group.
    /// Throws <see cref="InvalidOperationException"/> if peerId is not a member.
    /// Go reference: jetstream_cluster.go setPreferred / rg.Preferred assignment.
    /// </summary>
    public void SetPreferred(string peerId)
    {
        if (!IsMember(peerId))
            throw new InvalidOperationException(
                $"Cannot set preferred peer '{peerId}': not a member of RAFT group '{Name}'.");
        Preferred = peerId;
    }

    /// <summary>
    /// Removes a peer from the group. If the removed peer was the preferred peer,
    /// <see cref="Preferred"/> is cleared. Returns true if the peer was found and removed.
    /// Go reference: jetstream_cluster.go removePeer.
    /// </summary>
    public bool RemovePeer(string peerId)
    {
        var removed = Peers.Remove(peerId);
        if (removed && string.Equals(Preferred, peerId, StringComparison.Ordinal))
            Preferred = string.Empty;
        return removed;
    }

    /// <summary>
    /// Adds a peer to the group if not already present. Returns true if the peer was added.
    /// Go reference: jetstream_cluster.go addPeer / expandGroup.
    /// </summary>
    public bool AddPeer(string peerId)
    {
        if (IsMember(peerId))
            return false;
        Peers.Add(peerId);
        return true;
    }

    /// <summary>
    /// Factory method that uses <see cref="PlacementEngine.SelectPeerGroup"/> to create a
    /// <see cref="RaftGroup"/> with topology-aware peer selection and sets
    /// <see cref="DesiredReplicas"/> to the requested replica count.
    /// Go reference: jetstream_cluster.go createGroupForStream — calls selectPeerGroup then
    /// assigns rg.DesiredReplicas = replicas.
    /// </summary>
    public static RaftGroup CreateRaftGroup(
        string groupName,
        int replicas,
        IReadOnlyList<PeerInfo> availablePeers,
        PlacementPolicy? policy = null)
    {
        var group = PlacementEngine.SelectPeerGroup(groupName, replicas, availablePeers, policy);
        group.DesiredReplicas = replicas;
        return group;
    }
}

/// <summary>
/// Assignment of a stream to a RAFT group of peers.
/// Go reference: jetstream_cluster.go:166-184 streamAssignment struct.
/// </summary>
public sealed class StreamAssignment
{
    public required string StreamName { get; init; }
    public required RaftGroup Group { get; init; }
    public DateTime Created { get; init; } = DateTime.UtcNow;
    public string ConfigJson { get; set; } = "{}";
    public string SyncSubject { get; set; } = string.Empty;
    public bool Responded { get; set; }
    public bool Recovering { get; set; }
    public bool Reassigning { get; set; }

    /// <summary>
    /// Schema version for forward-compatibility. Assignments with a version greater than
    /// <see cref="JetStreamMetaGroup.CurrentVersion"/> are skipped so that an older node
    /// does not crash when replaying a RAFT log produced by a newer cluster peer.
    /// Version 0 is treated as equivalent to version 1 (backward compatibility with
    /// pre-versioned assignments).
    /// Go reference: jetstream_cluster.go — versioned assignment processing.
    /// </summary>
    public int Version { get; set; } = 1;

    /// <summary>
    /// Consumer assignments keyed by consumer name.
    /// Uses <see cref="JsonObjectCreationHandling.Populate"/> so the deserializer populates
    /// the existing dictionary instance rather than replacing it (the property has no setter).
    /// </summary>
    [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
    public Dictionary<string, ConsumerAssignment> Consumers { get; } = new(StringComparer.Ordinal);
}

/// <summary>
/// Assignment of a consumer to a RAFT group within a stream's cluster.
/// Go reference: jetstream_cluster.go:250-266 consumerAssignment struct.
/// </summary>
public sealed class ConsumerAssignment
{
    public required string ConsumerName { get; init; }
    public required string StreamName { get; init; }
    public required RaftGroup Group { get; init; }
    public DateTime Created { get; init; } = DateTime.UtcNow;
    public string ConfigJson { get; set; } = "{}";
    public bool Responded { get; set; }
    public bool Recovering { get; set; }

    /// <summary>
    /// Schema version for forward-compatibility. Assignments with a version greater than
    /// <see cref="JetStreamMetaGroup.CurrentVersion"/> are skipped so that an older node
    /// does not crash when replaying a RAFT log produced by a newer cluster peer.
    /// Version 0 is treated as equivalent to version 1 (backward compatibility with
    /// pre-versioned assignments).
    /// Go reference: jetstream_cluster.go — versioned assignment processing.
    /// </summary>
    public int Version { get; set; } = 1;
}
