//! Failure detector — runs in the coordinator pod.
//!
//! Periodically checks partition heartbeats stored in etcd and marks
//! partitions as `Failed` when no heartbeat has been received within
//! `heartbeat_timeout_secs`. The recovery manager then picks up failed
//! partitions and migrates them to healthy pods.
//!
//! ## Configuration defaults
//! - Check interval:       10 seconds
//! - Heartbeat timeout:    30 seconds (3 missed beats at 5s interval)

#[cfg(feature = "distributed")]
use std::sync::Arc;
#[cfg(feature = "distributed")]
use std::time::Duration;

#[cfg(feature = "distributed")]
use tokio::sync::RwLock;
#[cfg(feature = "distributed")]
use tokio::task::JoinHandle;
#[cfg(feature = "distributed")]
use tokio_util::sync::CancellationToken;

#[cfg(feature = "distributed")]
use crate::distributed::partition::location_service::{
    PartitionLocationService, PartitionStatus,
};

// ── Detector ──────────────────────────────────────────────────────────────────

/// Detects failed partitions by watching heartbeat timestamps.
#[cfg(feature = "distributed")]
pub struct FailureDetector {
    location_svc:          Arc<RwLock<PartitionLocationService>>,
    check_interval_secs:   u64,
    heartbeat_timeout_secs: u64,
    cancel:                CancellationToken,
    handle:                Option<JoinHandle<()>>,
}

#[cfg(feature = "distributed")]
impl FailureDetector {
    /// Create a new failure detector.
    ///
    /// - `check_interval_secs`:    how often to scan heartbeats.
    /// - `heartbeat_timeout_secs`: age threshold for marking a partition failed.
    pub fn new(
        location_svc:           Arc<RwLock<PartitionLocationService>>,
        check_interval_secs:    u64,
        heartbeat_timeout_secs: u64,
    ) -> Self {
        Self {
            location_svc,
            check_interval_secs,
            heartbeat_timeout_secs,
            cancel: CancellationToken::new(),
            handle: None,
        }
    }

    /// Start the detection loop in a background tokio task.
    pub fn start(&mut self) {
        if self.handle.is_some() {
            return; // already running
        }

        let svc     = Arc::clone(&self.location_svc);
        let timeout = self.heartbeat_timeout_secs;
        let interval = Duration::from_secs(self.check_interval_secs);
        let cancel  = self.cancel.clone();

        let handle = tokio::spawn(async move {
            let mut ticker = tokio::time::interval(interval);
            loop {
                tokio::select! {
                    _ = ticker.tick() => {
                        let stale = {
                            let guard = svc.read().await;
                            guard.stale_partitions(timeout).await
                        };

                        if !stale.is_empty() {
                            tracing::warn!(
                                partitions = ?stale,
                                "heartbeat timeout — marking partitions as failed"
                            );
                            let mut guard = svc.write().await;
                            for pid in stale {
                                if let Err(e) = guard
                                    .update_partition_status(pid, PartitionStatus::Failed)
                                    .await
                                {
                                    tracing::error!(
                                        partition = pid,
                                        error = %e,
                                        "failed to mark partition as failed"
                                    );
                                }
                            }
                        }
                    }
                    _ = cancel.cancelled() => {
                        tracing::info!("failure detector shutting down");
                        break;
                    }
                }
            }
        });

        self.handle = Some(handle);
    }

    /// Stop the detection loop gracefully.
    pub fn stop(&mut self) {
        self.cancel.cancel();
        self.handle = None;
    }

    /// Perform a single synchronous check and return failed partition IDs.
    ///
    /// Useful for testing or on-demand checks without starting the loop.
    pub async fn check_once(
        location_svc:           &Arc<RwLock<PartitionLocationService>>,
        heartbeat_timeout_secs: u64,
    ) -> Vec<u32> {
        let stale = {
            let guard = location_svc.read().await;
            guard.stale_partitions(heartbeat_timeout_secs).await
        };
        stale
    }
}

#[cfg(feature = "distributed")]
impl Drop for FailureDetector {
    fn drop(&mut self) {
        self.cancel.cancel();
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(all(test, feature = "distributed"))]
mod tests {
    use super::*;

    #[test]
    fn test_detector_defaults() {
        assert_eq!(Duration::from_secs(10).as_secs(), 10);
        assert_eq!(Duration::from_secs(30).as_secs(), 30);
    }

    #[test]
    fn test_cancellation_propagates() {
        let token = CancellationToken::new();
        let child = token.child_token();
        token.cancel();
        assert!(child.is_cancelled());
    }

    /// Verifies that stale partition detection works with mock timestamps.
    ///
    /// This test uses direct cache manipulation (via the public API) instead
    /// of a real etcd connection.
    #[tokio::test]
    #[ignore = "requires a running etcd server on localhost:2379"]
    async fn test_stale_detection_live() {
        use crate::distributed::partition::location_service::PartitionLocationService;

        let mut svc = PartitionLocationService::connect(
            vec!["http://127.0.0.1:2379".to_string()]
        ).await.unwrap();

        // Register with a fake old timestamp by registering and then not
        // sending heartbeats — but we can't inject old timestamps via the
        // public API, so this is integration-only.
        svc.register_partition(
            99,
            "ocg-test-pod".to_string(),
            "s3://test/p99".to_string(),
        ).await.unwrap();

        // Immediately after registration, should NOT be stale (just registered)
        let svc = Arc::new(RwLock::new(svc));
        let stale = FailureDetector::check_once(&svc, 0).await; // 0s timeout → all stale
        assert!(stale.contains(&99));
    }
}
