//! Metrics collection for cluster nodes.
//!
//! This module provides the [`MetricsCollector`] trait for collecting node metrics
//! that are reported to the cluster registry. Metrics enable load balancers to
//! make weighted routing decisions based on node load.
//!
//! # Built-in Collectors
//!
//! - [`NoopMetricsCollector`]: Returns empty metadata (default)
//! - `SystemMetricsCollector`: Collects CPU, memory, and load metrics (requires `system-metrics` feature)
//!
//! # Custom Collectors
//!
//! Implement [`MetricsCollector`] to provide application-specific metrics:
//!
//! ```ignore
//! use photon_etcd_cluster::metrics::{MetricsCollector, NoopMetricsCollector};
//! use photon_etcd_cluster::types::NodeMetadata;
//! use serde_json::json;
//!
//! struct MyAppMetrics {
//!     // Your application state
//! }
//!
//! impl MetricsCollector for MyAppMetrics {
//!     fn collect(&self) -> NodeMetadata {
//!         json!({
//!             "cpu_usage_percent": 45.0,
//!             "queue_depth": 10,
//!             "custom_metric": "value"
//!         })
//!     }
//! }
//! ```

use crate::types::NodeMetadata;
use serde_json::Value;

/// Trait for collecting node metrics.
///
/// Implement this trait to provide custom metric sources for your cluster nodes.
/// The collected metrics are stored in etcd and can be used by load balancers
/// for weighted routing decisions.
///
/// # Thread Safety
///
/// Implementations must be `Send + Sync` as the collector may be called from
/// multiple async tasks concurrently.
///
/// # Performance
///
/// The `collect()` method is called periodically (typically every few seconds).
/// Implementations should be efficient and avoid blocking operations.
/// If expensive computations are needed, consider caching results.
pub trait MetricsCollector: Send + Sync {
    /// Collects current metrics from the node.
    ///
    /// Returns a JSON value containing the metrics. Return `Value::Null` to
    /// indicate no metrics are available.
    ///
    /// # Common Metrics
    ///
    /// Use the keys from [`crate::types::metadata_keys`] for interoperability:
    /// - `cpu_usage_percent`: CPU usage (0.0 - 100.0)
    /// - `memory_usage_percent`: Memory usage (0.0 - 100.0)
    /// - `memory_available_bytes`: Available memory in bytes
    /// - `load_avg_1m`: 1-minute load average
    /// - `active_connections`: Active connection count
    /// - `requests_per_second`: Request rate
    /// - `queue_depth`: Request queue depth
    fn collect(&self) -> NodeMetadata;
}

/// A no-op metrics collector that returns null metadata.
///
/// This is the default collector used when no custom collector is provided.
/// It results in nodes being registered without any metrics, which causes
/// load balancers to use equal weighting for all nodes.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopMetricsCollector;

impl NoopMetricsCollector {
    /// Creates a new no-op metrics collector.
    pub fn new() -> Self {
        Self
    }
}

impl MetricsCollector for NoopMetricsCollector {
    fn collect(&self) -> NodeMetadata {
        Value::Null
    }
}

/// System metrics collector using the `sysinfo` crate.
///
/// Collects:
/// - CPU usage percentage
/// - Memory usage percentage
/// - Available memory in bytes
/// - 1-minute load average (on supported platforms)
///
/// # Feature Flag
///
/// This collector requires the `system-metrics` feature:
///
/// ```toml
/// [dependencies]
/// photon-etcd-cluster = { version = "0.1", features = ["system-metrics"] }
/// ```
///
/// # Example
///
/// ```ignore
/// use photon_etcd_cluster::metrics::SystemMetricsCollector;
/// use photon_etcd_cluster::ClusterNodeBuilder;
///
/// let collector = SystemMetricsCollector::new();
/// let node = ClusterNodeBuilder::new(endpoints, id, ip, group)
///     .metrics_collector(collector)
///     .build();
/// ```
#[cfg(feature = "system-metrics")]
pub struct SystemMetricsCollector {
    sys: std::sync::Mutex<sysinfo::System>,
}

#[cfg(feature = "system-metrics")]
impl SystemMetricsCollector {
    /// Creates a new system metrics collector.
    ///
    /// Initializes the underlying system information gatherer.
    /// The first call to `collect()` may return incomplete data as
    /// some metrics (like CPU usage) require a baseline measurement.
    pub fn new() -> Self {
        use sysinfo::System;
        Self {
            sys: std::sync::Mutex::new(System::new()),
        }
    }
}

#[cfg(feature = "system-metrics")]
impl Default for SystemMetricsCollector {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(feature = "system-metrics")]
impl MetricsCollector for SystemMetricsCollector {
    fn collect(&self) -> NodeMetadata {
        use crate::types::metadata_keys;
        use serde_json::json;
        use sysinfo::System;

        let mut sys = self.sys.lock().unwrap();

        // Refresh only the metrics we need
        sys.refresh_cpu_usage();
        sys.refresh_memory();

        let cpu_usage = sys.global_cpu_usage();
        let total_mem = sys.total_memory();
        let used_mem = sys.used_memory();
        let available_mem = sys.available_memory();

        let memory_usage_percent = if total_mem > 0 {
            (used_mem as f64 / total_mem as f64) * 100.0
        } else {
            0.0
        };

        // Get load average (returns LoadAvg struct with one, five, fifteen fields)
        let load_avg = System::load_average();

        json!({
            metadata_keys::CPU_USAGE_PERCENT: cpu_usage,
            metadata_keys::MEMORY_USAGE_PERCENT: memory_usage_percent,
            metadata_keys::MEMORY_AVAILABLE_BYTES: available_mem,
            metadata_keys::LOAD_AVG_1M: load_avg.one
        })
    }
}

#[cfg(feature = "system-metrics")]
impl std::fmt::Debug for SystemMetricsCollector {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SystemMetricsCollector").finish()
    }
}

/// A composite metrics collector that merges metrics from multiple sources.
///
/// Useful when you want to combine system metrics with application-specific metrics.
///
/// # Example
///
/// ```ignore
/// use photon_etcd_cluster::metrics::{CompositeMetricsCollector, SystemMetricsCollector};
///
/// let system = SystemMetricsCollector::new();
/// let app = MyAppMetrics::new();
///
/// let collector = CompositeMetricsCollector::new()
///     .with_collector(system)
///     .with_collector(app);
/// ```
pub struct CompositeMetricsCollector {
    collectors: Vec<Box<dyn MetricsCollector>>,
}

impl CompositeMetricsCollector {
    /// Creates a new empty composite collector.
    pub fn new() -> Self {
        Self {
            collectors: Vec::new(),
        }
    }

    /// Adds a metrics collector to the composite.
    ///
    /// Collectors are called in order, and their results are merged.
    /// Later collectors can override keys from earlier collectors.
    pub fn with_collector<C: MetricsCollector + 'static>(mut self, collector: C) -> Self {
        self.collectors.push(Box::new(collector));
        self
    }
}

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

impl MetricsCollector for CompositeMetricsCollector {
    fn collect(&self) -> NodeMetadata {
        if self.collectors.is_empty() {
            return Value::Null;
        }

        let mut result = serde_json::Map::new();

        for collector in &self.collectors {
            let metrics = collector.collect();
            if let Value::Object(map) = metrics {
                for (key, value) in map {
                    result.insert(key, value);
                }
            }
        }

        if result.is_empty() {
            Value::Null
        } else {
            Value::Object(result)
        }
    }
}

impl std::fmt::Debug for CompositeMetricsCollector {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CompositeMetricsCollector")
            .field("collector_count", &self.collectors.len())
            .finish()
    }
}

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

    #[test]
    fn test_noop_collector_returns_null() {
        let collector = NoopMetricsCollector::new();
        assert_eq!(collector.collect(), Value::Null);
    }

    #[test]
    fn test_noop_collector_default() {
        let collector = NoopMetricsCollector::default();
        assert_eq!(collector.collect(), Value::Null);
    }

    struct TestCollector {
        value: Value,
    }

    impl MetricsCollector for TestCollector {
        fn collect(&self) -> NodeMetadata {
            self.value.clone()
        }
    }

    #[test]
    fn test_composite_collector_empty() {
        let collector = CompositeMetricsCollector::new();
        assert_eq!(collector.collect(), Value::Null);
    }

    #[test]
    fn test_composite_collector_single() {
        let collector = CompositeMetricsCollector::new().with_collector(TestCollector {
            value: json!({"cpu": 50.0}),
        });

        let result = collector.collect();
        assert_eq!(result["cpu"], 50.0);
    }

    #[test]
    fn test_composite_collector_merge() {
        let collector = CompositeMetricsCollector::new()
            .with_collector(TestCollector {
                value: json!({"cpu": 50.0, "mem": 60.0}),
            })
            .with_collector(TestCollector {
                value: json!({"queue": 10, "cpu": 55.0}), // cpu overrides
            });

        let result = collector.collect();
        assert_eq!(result["cpu"], 55.0); // Overridden by second collector
        assert_eq!(result["mem"], 60.0);
        assert_eq!(result["queue"], 10);
    }

    #[test]
    fn test_composite_collector_with_null() {
        let collector = CompositeMetricsCollector::new()
            .with_collector(NoopMetricsCollector::new())
            .with_collector(TestCollector {
                value: json!({"cpu": 50.0}),
            });

        let result = collector.collect();
        assert_eq!(result["cpu"], 50.0);
    }

    #[cfg(feature = "system-metrics")]
    #[test]
    fn test_system_metrics_collector() {
        let collector = SystemMetricsCollector::new();

        // First collection establishes baseline
        let _ = collector.collect();

        // Wait a bit for CPU measurement
        std::thread::sleep(std::time::Duration::from_millis(100));

        let result = collector.collect();
        assert!(result.is_object());

        // Check that expected keys exist
        let obj = result.as_object().unwrap();
        assert!(obj.contains_key("cpu_usage_percent"));
        assert!(obj.contains_key("memory_usage_percent"));
        assert!(obj.contains_key("memory_available_bytes"));
        assert!(obj.contains_key("load_avg_1m"));

        // CPU and memory should be reasonable values
        let cpu = obj["cpu_usage_percent"].as_f64().unwrap();
        assert!(cpu >= 0.0 && cpu <= 100.0);

        let mem = obj["memory_usage_percent"].as_f64().unwrap();
        assert!(mem >= 0.0 && mem <= 100.0);
    }
}
