use std::{net::IpAddr, sync::atomic::{AtomicU8, Ordering}};

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Schema-less metadata for nodes.
///
/// This type alias provides maximum flexibility for arbitrary node metadata.
/// Nodes can report any JSON-serializable data (CPU load, memory, custom metrics, etc.)
/// and consumers (like load balancers) can interpret the metadata as needed.
///
/// # Common Metadata Keys
///
/// While the schema is flexible, these keys are commonly used:
/// - `cpu_usage_percent`: CPU usage as percentage (0.0 - 100.0)
/// - `memory_usage_percent`: Memory usage as percentage (0.0 - 100.0)
/// - `memory_available_bytes`: Available memory in bytes
/// - `load_avg_1m`: 1-minute load average
/// - `active_connections`: Number of active connections
/// - `requests_per_second`: Request rate
/// - `queue_depth`: Request queue depth
///
/// # Example
///
/// ```ignore
/// use serde_json::json;
///
/// let metadata = json!({
///     "cpu_usage_percent": 45.5,
///     "memory_usage_percent": 62.0,
///     "custom_metric": "value"
/// });
/// ```
pub type NodeMetadata = Value;

/// Well-known metadata keys for convenience.
pub mod metadata_keys {
    /// CPU usage as percentage (0.0 - 100.0)
    pub const CPU_USAGE_PERCENT: &str = "cpu_usage_percent";
    /// Memory usage as percentage (0.0 - 100.0)
    pub const MEMORY_USAGE_PERCENT: &str = "memory_usage_percent";
    /// Available memory in bytes
    pub const MEMORY_AVAILABLE_BYTES: &str = "memory_available_bytes";
    /// 1-minute load average
    pub const LOAD_AVG_1M: &str = "load_avg_1m";
    /// Active connections/requests being processed
    pub const ACTIVE_CONNECTIONS: &str = "active_connections";
    /// Requests per second
    pub const REQUESTS_PER_SECOND: &str = "requests_per_second";
    /// Request queue depth
    pub const QUEUE_DEPTH: &str = "queue_depth";
}

/// Represents a node in the cluster.
///
/// Each node has a unique identifier, network address, and timestamp
/// of when it was last seen by the cluster.
///
/// # Equality
///
/// Two nodes are considered equal if they have the same `id` and `ip`.
/// The `last_seen` timestamp and `metadata` are intentionally excluded from
/// equality checks to allow detecting actual state changes vs heartbeat updates.
///
/// # Metadata
///
/// The optional `metadata` field allows nodes to report arbitrary data
/// (CPU load, memory, custom metrics) that can be used by load balancers
/// for weighted routing decisions.
#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
pub struct Node {
    /// Unique identifier for this node
    pub id: String,
    /// Network address of this node
    pub ip: IpAddr,
    /// Unix timestamp (milliseconds) when this node was last seen
    pub last_seen: i64,
    /// Optional node metadata (CPU, memory, custom metrics)
    /// Defaults to null if not provided (backward compatible)
    #[serde(default, skip_serializing_if = "Value::is_null")]
    pub metadata: NodeMetadata,
}

impl PartialEq for Node {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id && self.ip == other.ip
    }
}

impl std::hash::Hash for Node {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.id.hash(state);
        self.ip.hash(state);
    }
}

/// Health status of a cluster node's connection to etcd.
#[repr(u8)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum HealthStatus {
    /// Initial state before first health check
    Unknown = 0,
    /// Connected and heartbeats are successful
    Healthy = 1,
    /// Connection issues or missed heartbeats
    Unhealthy = 2,
}

/// Thread-safe atomic wrapper for [`HealthStatus`].
#[derive(Debug)]
pub struct AtomicHealth(AtomicU8);

impl Default for AtomicHealth {
    fn default() -> Self {
        Self::new()
    }
}

impl AtomicHealth {
    /// Creates a new `AtomicHealth` with `Unknown` status.
    pub fn new() -> Self {
        Self(AtomicU8::new(HealthStatus::Unknown as u8))
    }

    /// Loads the current health status.
    pub fn load(&self) -> HealthStatus {
        match self.0.load(Ordering::Acquire) {
            1 => HealthStatus::Healthy,
            2 => HealthStatus::Unhealthy,
            _ => HealthStatus::Unknown,
        }
    }

    /// Stores a new health status.
    pub fn store(&self, status: HealthStatus) {
        self.0.store(status as u8, Ordering::Release);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_atomic_health_state_transitions() {
        let health = AtomicHealth::new();
        assert_eq!(health.load(), HealthStatus::Unknown);

        health.store(HealthStatus::Healthy);
        assert_eq!(health.load(), HealthStatus::Healthy);

        health.store(HealthStatus::Unhealthy);
        assert_eq!(health.load(), HealthStatus::Unhealthy);

        // Can transition back to healthy
        health.store(HealthStatus::Healthy);
        assert_eq!(health.load(), HealthStatus::Healthy);
    }

    #[test]
    fn test_atomic_health_default() {
        let health = AtomicHealth::default();
        assert_eq!(health.load(), HealthStatus::Unknown);
    }
}
