//! Backend Health Checker for FiberLB
//!
//! Performs active health checks on backends and updates their status.

use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use tokio::net::TcpStream;
use tokio::sync::watch;
use tokio::time::{interval, timeout};

use crate::metadata::LbMetadataStore;
use fiberlb_types::{Backend, BackendStatus, HealthCheck, HealthCheckType};

/// Result type for health check operations
pub type Result<T> = std::result::Result<T, HealthCheckError>;

/// Health check error types
#[derive(Debug, thiserror::Error)]
pub enum HealthCheckError {
    #[error("Connection failed: {0}")]
    ConnectionFailed(String),
    #[error("Timeout")]
    Timeout,
    #[error("HTTP error: {0}")]
    HttpError(String),
    #[error("Metadata error: {0}")]
    MetadataError(String),
}

/// Backend Health Checker
pub struct HealthChecker {
    metadata: Arc<LbMetadataStore>,
    check_interval: Duration,
    check_timeout: Duration,
    shutdown_rx: watch::Receiver<bool>,
}

impl HealthChecker {
    /// Create a new health checker
    pub fn new(
        metadata: Arc<LbMetadataStore>,
        check_interval: Duration,
        shutdown_rx: watch::Receiver<bool>,
    ) -> Self {
        Self {
            metadata,
            check_interval,
            check_timeout: Duration::from_secs(5),
            shutdown_rx,
        }
    }

    /// Create with custom timeout
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.check_timeout = timeout;
        self
    }

    /// Run the health check loop
    pub async fn run(&mut self) {
        let mut ticker = interval(self.check_interval);

        loop {
            tokio::select! {
                _ = ticker.tick() => {
                    if let Err(e) = self.check_all_backends().await {
                        tracing::warn!("Health check cycle error: {}", e);
                    }
                }
                _ = self.shutdown_rx.changed() => {
                    if *self.shutdown_rx.borrow() {
                        tracing::info!("Health checker shutting down");
                        break;
                    }
                }
            }
        }
    }

    /// Check all backends across all pools
    async fn check_all_backends(&self) -> Result<()> {
        // Get all load balancers
        let lbs = self
            .metadata
            .list_all_lbs()
            .await
            .map_err(|e| HealthCheckError::MetadataError(e.to_string()))?;

        for lb in lbs {
            // Get all pools for this LB
            let pools = self
                .metadata
                .list_pools(&lb.id)
                .await
                .map_err(|e| HealthCheckError::MetadataError(e.to_string()))?;

            for pool in pools {
                // Get health check config for this pool (if any)
                let health_checks = self
                    .metadata
                    .list_health_checks(&pool.id)
                    .await
                    .map_err(|e| HealthCheckError::MetadataError(e.to_string()))?;

                // Use the first enabled health check config, or default TCP check.
                let hc_config = health_checks.into_iter().find(|check| check.enabled);

                // Check all backends in the pool
                let backends = self
                    .metadata
                    .list_backends(&pool.id)
                    .await
                    .map_err(|e| HealthCheckError::MetadataError(e.to_string()))?;

                for backend in backends {
                    let status = self.check_backend(&backend, hc_config.as_ref()).await;

                    // Update backend status
                    if let Err(e) = self
                        .metadata
                        .update_backend_health(&pool.id, &backend.id, status)
                        .await
                    {
                        tracing::debug!("Failed to update backend {} status: {}", backend.id, e);
                    }
                }
            }
        }

        Ok(())
    }

    /// Check a single backend
    async fn check_backend(
        &self,
        backend: &Backend,
        hc_config: Option<&HealthCheck>,
    ) -> BackendStatus {
        let check_type = hc_config
            .map(|hc| hc.check_type)
            .unwrap_or(HealthCheckType::Tcp);

        let result = match check_type {
            HealthCheckType::Tcp => self.tcp_check(backend).await,
            HealthCheckType::Http => {
                let path = hc_config
                    .and_then(|hc| hc.http_config.as_ref())
                    .map(|cfg| cfg.path.as_str())
                    .unwrap_or("/health");
                self.http_check(backend, path).await
            }
            HealthCheckType::Https => {
                // For now, treat HTTPS same as HTTP (no TLS verification)
                let path = hc_config
                    .and_then(|hc| hc.http_config.as_ref())
                    .map(|cfg| cfg.path.as_str())
                    .unwrap_or("/health");
                self.http_check(backend, path).await
            }
            HealthCheckType::Udp | HealthCheckType::Ping => {
                // Not implemented - assume healthy
                Ok(())
            }
        };

        match result {
            Ok(()) => {
                tracing::trace!("Backend {} is healthy", backend.id);
                BackendStatus::Online
            }
            Err(e) => {
                tracing::debug!("Backend {} health check failed: {}", backend.id, e);
                BackendStatus::Offline
            }
        }
    }

    /// TCP health check - attempt to connect
    async fn tcp_check(&self, backend: &Backend) -> Result<()> {
        let addr: SocketAddr = format!("{}:{}", backend.address, backend.port)
            .parse()
            .map_err(|e| HealthCheckError::ConnectionFailed(format!("invalid address: {}", e)))?;

        timeout(self.check_timeout, TcpStream::connect(addr))
            .await
            .map_err(|_| HealthCheckError::Timeout)?
            .map_err(|e| HealthCheckError::ConnectionFailed(e.to_string()))?;

        Ok(())
    }

    /// HTTP health check - GET request and check status code
    async fn http_check(&self, backend: &Backend, path: &str) -> Result<()> {
        let addr: SocketAddr = format!("{}:{}", backend.address, backend.port)
            .parse()
            .map_err(|e| HealthCheckError::ConnectionFailed(format!("invalid address: {}", e)))?;

        // Connect with timeout
        let stream = timeout(self.check_timeout, TcpStream::connect(addr))
            .await
            .map_err(|_| HealthCheckError::Timeout)?
            .map_err(|e| HealthCheckError::ConnectionFailed(e.to_string()))?;

        // Send minimal HTTP GET request
        let request = format!(
            "GET {} HTTP/1.1\r\nHost: {}:{}\r\nConnection: close\r\n\r\n",
            path, backend.address, backend.port
        );

        // Write request
        stream
            .writable()
            .await
            .map_err(|e| HealthCheckError::HttpError(format!("stream not writable: {}", e)))?;

        match stream.try_write(request.as_bytes()) {
            Ok(_) => {}
            Err(e) => {
                return Err(HealthCheckError::HttpError(format!("write failed: {}", e)));
            }
        }

        // Read response (just first line for status code)
        let mut buf = [0u8; 128];
        stream
            .readable()
            .await
            .map_err(|e| HealthCheckError::HttpError(format!("stream not readable: {}", e)))?;

        let n = match stream.try_read(&mut buf) {
            Ok(n) => n,
            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
                // Wait a bit and try again
                tokio::time::sleep(Duration::from_millis(100)).await;
                stream
                    .try_read(&mut buf)
                    .map_err(|e| HealthCheckError::HttpError(format!("read failed: {}", e)))?
            }
            Err(e) => {
                return Err(HealthCheckError::HttpError(format!("read failed: {}", e)));
            }
        };

        if n == 0 {
            return Err(HealthCheckError::HttpError("empty response".into()));
        }

        // Parse status line
        let response = String::from_utf8_lossy(&buf[..n]);
        let status_line = response.lines().next().unwrap_or("");

        // Check for 2xx status code
        if status_line.contains(" 200 ")
            || status_line.contains(" 201 ")
            || status_line.contains(" 202 ")
            || status_line.contains(" 204 ")
        {
            Ok(())
        } else {
            Err(HealthCheckError::HttpError(format!(
                "unhealthy status: {}",
                status_line
            )))
        }
    }
}

/// Start a health checker in the background
pub fn spawn_health_checker(
    metadata: Arc<LbMetadataStore>,
    check_interval: Duration,
    check_timeout: Duration,
) -> (tokio::task::JoinHandle<()>, watch::Sender<bool>) {
    let (shutdown_tx, shutdown_rx) = watch::channel(false);

    let handle = tokio::spawn(async move {
        let mut checker =
            HealthChecker::new(metadata, check_interval, shutdown_rx).with_timeout(check_timeout);
        checker.run().await;
    });

    (handle, shutdown_tx)
}

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

    #[tokio::test]
    async fn test_health_checker_creation() {
        let metadata = Arc::new(LbMetadataStore::new_in_memory());
        let (_tx, rx) = watch::channel(false);
        let checker = HealthChecker::new(metadata, Duration::from_secs(5), rx);

        assert_eq!(checker.check_interval, Duration::from_secs(5));
        assert_eq!(checker.check_timeout, Duration::from_secs(5));
    }

    #[tokio::test]
    async fn test_tcp_check_unreachable() {
        let metadata = Arc::new(LbMetadataStore::new_in_memory());
        let (_tx, rx) = watch::channel(false);
        let checker = HealthChecker::new(metadata, Duration::from_secs(5), rx)
            .with_timeout(Duration::from_millis(100));

        let backend = Backend::new("test", PoolId::new(), "127.0.0.1", 59999);

        let result = checker.tcp_check(&backend).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_check_backend_returns_offline_on_failure() {
        let metadata = Arc::new(LbMetadataStore::new_in_memory());
        let (_tx, rx) = watch::channel(false);
        let checker = HealthChecker::new(metadata, Duration::from_secs(5), rx)
            .with_timeout(Duration::from_millis(100));

        let backend = Backend::new("test", PoolId::new(), "127.0.0.1", 59999);

        let status = checker.check_backend(&backend, None).await;
        assert_eq!(status, BackendStatus::Offline);
    }

    #[tokio::test]
    async fn test_spawn_health_checker() {
        let metadata = Arc::new(LbMetadataStore::new_in_memory());
        let (handle, shutdown_tx) =
            spawn_health_checker(metadata, Duration::from_secs(60), Duration::from_secs(5));

        // Verify it started
        assert!(!handle.is_finished());

        // Shutdown
        let _ = shutdown_tx.send(true);

        // Wait for shutdown with timeout
        let _ = tokio::time::timeout(Duration::from_secs(1), handle).await;
    }
}
