//! Membership type definitions for cluster node management.
//!
//! This module defines the core types for representing nodes in a StemeDB cluster:
//!
//! - [`NodeId`]: Unique identifier for each node (UUID-based)
//! - [`NodeInfo`]: Complete information about a node including addresses
//! - [`NodeState`]: Current perceived state of a node (alive, suspect, dead)
//! - [`MembershipEvent`]: Events emitted when membership changes

use serde::{Deserialize, Serialize};
use std::fmt;
use std::net::SocketAddr;
use uuid::Uuid;

use crate::sharding::ShardId;

/// Unique identifier for a node in the cluster.
///
/// Based on UUID v4 for global uniqueness without coordination.
/// Stored as 16 bytes for efficient serialization and comparison.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId([u8; 16]);

impl NodeId {
    /// Creates a new random NodeId using UUID v4.
    #[must_use]
    pub fn random() -> Self {
        Self(*Uuid::new_v4().as_bytes())
    }

    /// Creates a NodeId from a UUID.
    #[must_use]
    pub fn from_uuid(uuid: Uuid) -> Self {
        Self(*uuid.as_bytes())
    }

    /// Converts this NodeId to a UUID.
    #[must_use]
    pub fn to_uuid(&self) -> Uuid {
        Uuid::from_bytes(self.0)
    }

    /// Creates a NodeId from raw bytes.
    #[must_use]
    pub fn from_bytes(bytes: [u8; 16]) -> Self {
        Self(bytes)
    }

    /// Returns the raw bytes of this NodeId.
    #[must_use]
    pub fn as_bytes(&self) -> &[u8; 16] {
        &self.0
    }

    /// Returns a short hex representation (first 8 chars) for logging.
    #[must_use]
    pub fn short_hex(&self) -> String {
        hex::encode(&self.0[..4])
    }
}

impl fmt::Display for NodeId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.to_uuid())
    }
}

impl Default for NodeId {
    fn default() -> Self {
        Self::random()
    }
}

/// Complete information about a node in the cluster.
///
/// Contains the node's identity, network addresses, and current shard assignments.
/// This is exchanged during membership gossip to allow nodes to route requests.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeInfo {
    /// Unique identifier for this node.
    pub id: NodeId,

    /// Address for RPC communication (gRPC sync protocol).
    pub rpc_addr: SocketAddr,

    /// Address for HTTP API (client-facing).
    pub api_addr: SocketAddr,

    /// Shards this node is responsible for.
    ///
    /// A node may be the leader or a follower for each shard in this list.
    pub shard_assignments: Vec<ShardId>,

    /// Incarnation number for crashing/rejoining detection.
    ///
    /// Incremented each time the node restarts. Higher incarnation numbers
    /// override lower ones to handle the case where a node crashes and
    /// rejoins before failure detection completes.
    pub incarnation: u64,

    /// Optional metadata about this node.
    ///
    /// Can include things like datacenter, rack, or version information.
    pub metadata: Option<NodeMetadata>,
}

impl NodeInfo {
    /// Creates a new NodeInfo with the minimum required fields.
    #[must_use]
    pub fn new(id: NodeId, rpc_addr: SocketAddr, api_addr: SocketAddr) -> Self {
        Self {
            id,
            rpc_addr,
            api_addr,
            shard_assignments: Vec::new(),
            incarnation: 0,
            metadata: None,
        }
    }

    /// Returns the node's unique identifier.
    #[must_use]
    pub fn id(&self) -> NodeId {
        self.id
    }

    /// Adds a shard assignment to this node.
    pub fn assign_shard(&mut self, shard_id: ShardId) {
        if !self.shard_assignments.contains(&shard_id) {
            self.shard_assignments.push(shard_id);
        }
    }

    /// Removes a shard assignment from this node.
    pub fn unassign_shard(&mut self, shard_id: ShardId) {
        self.shard_assignments.retain(|&s| s != shard_id);
    }

    /// Increments the incarnation number (called on node restart).
    pub fn increment_incarnation(&mut self) {
        self.incarnation = self.incarnation.saturating_add(1);
    }
}

/// Optional metadata about a node.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeMetadata {
    /// Datacenter or region this node is in.
    pub datacenter: Option<String>,

    /// Rack or availability zone.
    pub rack: Option<String>,

    /// Software version running on this node.
    pub version: Option<String>,

    /// Custom key-value tags.
    pub tags: Vec<(String, String)>,
}

/// Current perceived state of a node.
///
/// States progress through: `Alive` -> `Suspect` -> `Dead` -> `Left`
///
/// The SWIM protocol uses a suspicion mechanism to avoid false positives
/// from transient network issues. A node is only marked dead after the
/// suspicion timeout expires without hearing from it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum NodeState {
    /// Node is responding to probes and considered healthy.
    Alive,

    /// Node has failed to respond to direct probe, but indirect probes
    /// are in progress. May recover to Alive or progress to Dead.
    Suspect,

    /// Node has been confirmed failed after suspicion timeout.
    /// May be removed from membership after grace period.
    Dead,

    /// Node has gracefully left the cluster.
    /// Different from Dead in that it was intentional.
    Left,
}

impl NodeState {
    /// Returns true if this node is considered available for routing.
    #[must_use]
    pub fn is_available(&self) -> bool {
        matches!(self, NodeState::Alive)
    }

    /// Returns true if this node should be removed from membership.
    #[must_use]
    pub fn should_remove(&self) -> bool {
        matches!(self, NodeState::Dead | NodeState::Left)
    }
}

impl fmt::Display for NodeState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            NodeState::Alive => write!(f, "alive"),
            NodeState::Suspect => write!(f, "suspect"),
            NodeState::Dead => write!(f, "dead"),
            NodeState::Left => write!(f, "left"),
        }
    }
}

/// Events emitted when cluster membership changes.
///
/// Subscribe to these events to react to cluster topology changes,
/// such as triggering anti-entropy sync when a new node joins.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MembershipEvent {
    /// A new node has joined the cluster.
    NodeJoined(NodeInfo),

    /// A node is suspected of being failed (probes timing out).
    NodeSuspected(NodeId),

    /// A node has been confirmed failed.
    NodeFailed(NodeId),

    /// A node has gracefully left the cluster.
    NodeLeft(NodeId),

    /// A node's information has been updated (e.g., shard assignments changed).
    NodeUpdated(NodeInfo),
}

impl MembershipEvent {
    /// Returns the NodeId associated with this event.
    #[must_use]
    pub fn node_id(&self) -> NodeId {
        match self {
            MembershipEvent::NodeJoined(info) => info.id,
            MembershipEvent::NodeSuspected(id) => *id,
            MembershipEvent::NodeFailed(id) => *id,
            MembershipEvent::NodeLeft(id) => *id,
            MembershipEvent::NodeUpdated(info) => info.id,
        }
    }

    /// Returns true if this is a join event.
    #[must_use]
    pub fn is_join(&self) -> bool {
        matches!(self, MembershipEvent::NodeJoined(_))
    }

    /// Returns true if this is a failure-related event.
    #[must_use]
    pub fn is_failure(&self) -> bool {
        matches!(self, MembershipEvent::NodeFailed(_) | MembershipEvent::NodeSuspected(_))
    }

    /// Returns true if this is a leave event.
    #[must_use]
    pub fn is_leave(&self) -> bool {
        matches!(self, MembershipEvent::NodeLeft(_))
    }
}

impl fmt::Display for MembershipEvent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MembershipEvent::NodeJoined(info) => {
                write!(f, "NodeJoined({})", info.id.short_hex())
            }
            MembershipEvent::NodeSuspected(id) => {
                write!(f, "NodeSuspected({})", id.short_hex())
            }
            MembershipEvent::NodeFailed(id) => {
                write!(f, "NodeFailed({})", id.short_hex())
            }
            MembershipEvent::NodeLeft(id) => {
                write!(f, "NodeLeft({})", id.short_hex())
            }
            MembershipEvent::NodeUpdated(info) => {
                write!(f, "NodeUpdated({})", info.id.short_hex())
            }
        }
    }
}

/// A timestamped membership entry for gossip propagation.
///
/// Combines node info with state and a logical clock for ordering.
/// Used internally by the SWIM protocol for gossip messages.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MembershipEntry {
    /// Node information.
    pub node: NodeInfo,

    /// Current perceived state.
    pub state: NodeState,

    /// Lamport timestamp for ordering updates.
    pub lamport_time: u64,
}

impl MembershipEntry {
    /// Creates a new membership entry.
    #[must_use]
    pub fn new(node: NodeInfo, state: NodeState, lamport_time: u64) -> Self {
        Self { node, state, lamport_time }
    }

    /// Returns true if this entry is newer than another for the same node.
    ///
    /// Uses incarnation number first, then lamport time for ordering.
    #[must_use]
    pub fn is_newer_than(&self, other: &Self) -> bool {
        if self.node.incarnation != other.node.incarnation {
            self.node.incarnation > other.node.incarnation
        } else {
            self.lamport_time > other.lamport_time
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{IpAddr, Ipv4Addr};

    fn test_addr(port: u16) -> SocketAddr {
        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port)
    }

    #[test]
    fn test_node_id_random_uniqueness() {
        let id1 = NodeId::random();
        let id2 = NodeId::random();
        assert_ne!(id1, id2);
    }

    #[test]
    fn test_node_id_uuid_roundtrip() {
        let uuid = Uuid::new_v4();
        let id = NodeId::from_uuid(uuid);
        assert_eq!(id.to_uuid(), uuid);
    }

    #[test]
    fn test_node_id_display() {
        let id = NodeId::random();
        let display = format!("{}", id);
        // Should be a valid UUID string
        assert!(Uuid::parse_str(&display).is_ok());
    }

    #[test]
    fn test_node_info_shard_assignment() {
        let mut info = NodeInfo::new(NodeId::random(), test_addr(9090), test_addr(8080));

        info.assign_shard(1);
        info.assign_shard(2);
        info.assign_shard(1); // Duplicate, should not add

        assert_eq!(info.shard_assignments.len(), 2);
        assert!(info.shard_assignments.contains(&1));
        assert!(info.shard_assignments.contains(&2));

        info.unassign_shard(1);
        assert_eq!(info.shard_assignments.len(), 1);
        assert!(!info.shard_assignments.contains(&1));
    }

    #[test]
    fn test_node_state_availability() {
        assert!(NodeState::Alive.is_available());
        assert!(!NodeState::Suspect.is_available());
        assert!(!NodeState::Dead.is_available());
        assert!(!NodeState::Left.is_available());
    }

    #[test]
    fn test_node_state_removal() {
        assert!(!NodeState::Alive.should_remove());
        assert!(!NodeState::Suspect.should_remove());
        assert!(NodeState::Dead.should_remove());
        assert!(NodeState::Left.should_remove());
    }

    #[test]
    fn test_membership_event_node_id() {
        let id = NodeId::random();
        let info = NodeInfo::new(id, test_addr(9090), test_addr(8080));

        let events = vec![
            MembershipEvent::NodeJoined(info.clone()),
            MembershipEvent::NodeSuspected(id),
            MembershipEvent::NodeFailed(id),
            MembershipEvent::NodeLeft(id),
            MembershipEvent::NodeUpdated(info),
        ];

        for event in events {
            assert_eq!(event.node_id(), id);
        }
    }

    #[test]
    fn test_membership_entry_ordering() {
        let id = NodeId::random();
        let mut node1 = NodeInfo::new(id, test_addr(9090), test_addr(8080));
        node1.incarnation = 1;

        let mut node2 = node1.clone();
        node2.incarnation = 2;

        let entry1 = MembershipEntry::new(node1.clone(), NodeState::Alive, 100);
        let entry2 = MembershipEntry::new(node2, NodeState::Alive, 50);

        // Higher incarnation wins even with lower lamport time
        assert!(entry2.is_newer_than(&entry1));

        // Same incarnation, higher lamport wins
        let entry3 = MembershipEntry::new(node1.clone(), NodeState::Alive, 200);
        assert!(entry3.is_newer_than(&entry1));
    }
}
