use crate::node::NodeId;
use crate::registry::InspectorRegistry;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use sysinfo::{Pid, ProcessesToUpdate, System};
use tokio::task::JoinHandle;
use tracing::{debug, warn};

/// Information about a monitored OS process.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProcessInfo {
    pub pid: u32,
    pub name: String,
    pub command: Vec<String>,
    pub exe: Option<PathBuf>,
    pub cwd: Option<PathBuf>,
    pub status: ProcessStatus,
    pub cpu_usage_percent: f32,
    pub memory_bytes: u64,
    pub virtual_memory_bytes: u64,
    pub start_time_secs: u64,
    pub run_time_secs: u64,
    pub disk_read_bytes: u64,
    pub disk_written_bytes: u64,
}

/// Simplified cross-platform process status.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum ProcessStatus {
    Running,
    Sleeping,
    Stopped,
    Zombie,
    Dead,
    Unknown,
}

impl From<sysinfo::ProcessStatus> for ProcessStatus {
    fn from(s: sysinfo::ProcessStatus) -> Self {
        match s {
            sysinfo::ProcessStatus::Run => ProcessStatus::Running,
            sysinfo::ProcessStatus::Sleep | sysinfo::ProcessStatus::Idle => ProcessStatus::Sleeping,
            sysinfo::ProcessStatus::Stop | sysinfo::ProcessStatus::Tracing => {
                ProcessStatus::Stopped
            }
            sysinfo::ProcessStatus::Zombie => ProcessStatus::Zombie,
            sysinfo::ProcessStatus::Dead | sysinfo::ProcessStatus::UninterruptibleDiskSleep => {
                ProcessStatus::Dead
            }
            _ => ProcessStatus::Unknown,
        }
    }
}

/// Direction of the last observed I/O activity.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum IoDirection {
    Read,
    Write,
}

/// Information about the most recent I/O activity.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IoActivity {
    pub direction: IoDirection,
    pub timestamp: chrono::DateTime<chrono::Utc>,
}

/// Monitors a set of OS processes by PID, providing metrics via `sysinfo`.
///
/// Each tracked PID is associated with a `NodeId` in the inspector tree.
/// The monitor can be polled manually or run as a background task that
/// periodically refreshes and updates the registry.
pub struct ProcessMonitor {
    system: System,
    tracked: HashMap<u32, NodeId>,
}

impl ProcessMonitor {
    /// Create a new process monitor.
    pub fn new() -> Self {
        Self {
            system: System::new(),
            tracked: HashMap::new(),
        }
    }

    /// Start tracking a process by PID, associated with the given node ID.
    pub fn track(&mut self, pid: u32, node_id: NodeId) {
        debug!(pid, node_id = %node_id, "Tracking process");
        self.tracked.insert(pid, node_id);
    }

    /// Stop tracking a process.
    pub fn untrack(&mut self, pid: u32) {
        debug!(pid, "Untracking process");
        self.tracked.remove(&pid);
    }

    /// Get the set of tracked PIDs and their node IDs.
    pub fn tracked_pids(&self) -> &HashMap<u32, NodeId> {
        &self.tracked
    }

    /// Refresh data for all tracked processes and return their info.
    pub fn refresh(&mut self) -> HashMap<u32, ProcessInfo> {
        // Build list of PIDs to refresh
        let pids: Vec<Pid> = self.tracked.keys().map(|&p| Pid::from_u32(p)).collect();

        // Refresh only tracked processes
        self.system
            .refresh_processes(ProcessesToUpdate::Some(&pids), true);

        let mut results = HashMap::new();

        for (&pid, _node_id) in &self.tracked {
            let sysinfo_pid = Pid::from_u32(pid);
            if let Some(process) = self.system.process(sysinfo_pid) {
                let disk_usage = process.disk_usage();
                let info = ProcessInfo {
                    pid,
                    name: process.name().to_string_lossy().to_string(),
                    command: process
                        .cmd()
                        .iter()
                        .map(|s| s.to_string_lossy().to_string())
                        .collect(),
                    exe: process.exe().map(|p| p.to_path_buf()),
                    cwd: process.cwd().map(|p| p.to_path_buf()),
                    status: ProcessStatus::from(process.status()),
                    cpu_usage_percent: process.cpu_usage(),
                    memory_bytes: process.memory(),
                    virtual_memory_bytes: process.virtual_memory(),
                    start_time_secs: process.start_time(),
                    run_time_secs: process.run_time(),
                    disk_read_bytes: disk_usage.read_bytes,
                    disk_written_bytes: disk_usage.written_bytes,
                };
                results.insert(pid, info);
            }
        }

        results
    }

    /// Get info for a single tracked process.
    pub fn get(&mut self, pid: u32) -> Option<ProcessInfo> {
        let sysinfo_pid = Pid::from_u32(pid);
        self.system
            .refresh_processes(ProcessesToUpdate::Some(&[sysinfo_pid]), true);

        self.system.process(sysinfo_pid).map(|process| {
            let disk_usage = process.disk_usage();
            ProcessInfo {
                pid,
                name: process.name().to_string_lossy().to_string(),
                command: process
                    .cmd()
                    .iter()
                    .map(|s| s.to_string_lossy().to_string())
                    .collect(),
                exe: process.exe().map(|p| p.to_path_buf()),
                cwd: process.cwd().map(|p| p.to_path_buf()),
                status: ProcessStatus::from(process.status()),
                cpu_usage_percent: process.cpu_usage(),
                memory_bytes: process.memory(),
                virtual_memory_bytes: process.virtual_memory(),
                start_time_secs: process.start_time(),
                run_time_secs: process.run_time(),
                disk_read_bytes: disk_usage.read_bytes,
                disk_written_bytes: disk_usage.written_bytes,
            }
        })
    }

    /// Check if a process is alive (outrospective: queries the OS directly).
    pub fn is_alive(&mut self, pid: u32) -> bool {
        let sysinfo_pid = Pid::from_u32(pid);
        self.system
            .refresh_processes(ProcessesToUpdate::Some(&[sysinfo_pid]), true);
        self.system.process(sysinfo_pid).is_some()
    }

    /// Spawn a background task that periodically refreshes tracked processes
    /// and updates their nodes in the registry.
    ///
    /// The task runs until the returned `JoinHandle` is aborted or the
    /// monitor is dropped.
    pub fn start_polling(
        mut self,
        registry: Arc<InspectorRegistry>,
        interval: Duration,
    ) -> JoinHandle<()> {
        tokio::spawn(async move {
            let mut ticker = tokio::time::interval(interval);
            loop {
                ticker.tick().await;

                let infos = self.refresh();

                for (pid, info) in &infos {
                    if let Some(node_id) = self.tracked.get(pid) {
                        let mut props = HashMap::new();
                        props.insert("pid".to_string(), serde_json::json!(info.pid));
                        props.insert("name".to_string(), serde_json::json!(info.name));
                        props.insert("command".to_string(), serde_json::json!(info.command));
                        props.insert(
                            "status".to_string(),
                            serde_json::to_value(&info.status).unwrap_or_default(),
                        );
                        props.insert(
                            "cpu_usage_percent".to_string(),
                            serde_json::json!(info.cpu_usage_percent),
                        );
                        props.insert(
                            "memory_bytes".to_string(),
                            serde_json::json!(info.memory_bytes),
                        );
                        props.insert(
                            "virtual_memory_bytes".to_string(),
                            serde_json::json!(info.virtual_memory_bytes),
                        );
                        props.insert(
                            "run_time_secs".to_string(),
                            serde_json::json!(info.run_time_secs),
                        );
                        props.insert(
                            "disk_read_bytes".to_string(),
                            serde_json::json!(info.disk_read_bytes),
                        );
                        props.insert(
                            "disk_written_bytes".to_string(),
                            serde_json::json!(info.disk_written_bytes),
                        );

                        if let Err(e) = registry.update_properties(node_id, props).await {
                            warn!(
                                pid,
                                node_id = %node_id,
                                error = %e,
                                "Failed to update process node properties"
                            );
                        }
                    }
                }

                // Check for dead processes
                let dead_pids: Vec<u32> = self
                    .tracked
                    .keys()
                    .filter(|&&pid| !infos.contains_key(&pid))
                    .copied()
                    .collect();

                for pid in dead_pids {
                    if let Some(node_id) = self.tracked.get(&pid) {
                        debug!(pid, node_id = %node_id, "Process no longer alive");
                        let _ = registry
                            .update_state(node_id, crate::node::NodeState::Stopped)
                            .await;
                    }
                }
            }
        })
    }
}

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

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

    #[test]
    fn test_process_status_from_sysinfo() {
        assert_eq!(
            ProcessStatus::from(sysinfo::ProcessStatus::Run),
            ProcessStatus::Running
        );
        assert_eq!(
            ProcessStatus::from(sysinfo::ProcessStatus::Sleep),
            ProcessStatus::Sleeping
        );
        assert_eq!(
            ProcessStatus::from(sysinfo::ProcessStatus::Zombie),
            ProcessStatus::Zombie
        );
        assert_eq!(
            ProcessStatus::from(sysinfo::ProcessStatus::Stop),
            ProcessStatus::Stopped
        );
    }

    #[test]
    fn test_process_monitor_track_untrack() {
        let mut monitor = ProcessMonitor::new();
        let node_id = NodeId::new("dirigent/test/proc");

        monitor.track(1234, node_id.clone());
        assert!(monitor.tracked_pids().contains_key(&1234));

        monitor.untrack(1234);
        assert!(!monitor.tracked_pids().contains_key(&1234));
    }

    #[test]
    fn test_process_monitor_refresh_current_process() {
        let mut monitor = ProcessMonitor::new();
        let current_pid = std::process::id();
        let node_id = NodeId::new("dirigent/test/self");

        monitor.track(current_pid, node_id);

        let infos = monitor.refresh();
        assert!(
            infos.contains_key(&current_pid),
            "Current process should be found"
        );

        let info = &infos[&current_pid];
        assert_eq!(info.pid, current_pid);
        assert!(!info.name.is_empty());
        assert!(info.memory_bytes > 0);
    }

    #[test]
    fn test_process_monitor_get_current_process() {
        let mut monitor = ProcessMonitor::new();
        let current_pid = std::process::id();

        let info = monitor.get(current_pid);
        assert!(info.is_some(), "Should find current process");

        let info = info.unwrap();
        assert_eq!(info.pid, current_pid);
    }

    #[test]
    fn test_process_monitor_is_alive() {
        let mut monitor = ProcessMonitor::new();

        // Current process should be alive
        assert!(monitor.is_alive(std::process::id()));

        // A very high PID should not exist
        assert!(!monitor.is_alive(u32::MAX));
    }

    #[test]
    fn test_process_info_serialization() {
        let info = ProcessInfo {
            pid: 1234,
            name: "test".to_string(),
            command: vec!["test".to_string(), "--flag".to_string()],
            exe: Some(PathBuf::from("/usr/bin/test")),
            cwd: Some(PathBuf::from("/home/user")),
            status: ProcessStatus::Running,
            cpu_usage_percent: 12.5,
            memory_bytes: 1024 * 1024,
            virtual_memory_bytes: 2048 * 1024,
            start_time_secs: 1700000000,
            run_time_secs: 3600,
            disk_read_bytes: 1024,
            disk_written_bytes: 2048,
        };

        let json = serde_json::to_string(&info).unwrap();
        let deserialized: ProcessInfo = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.pid, 1234);
        assert_eq!(deserialized.status, ProcessStatus::Running);
        assert_eq!(deserialized.memory_bytes, 1024 * 1024);
    }
}
