use std::{
    collections::HashMap,
    ffi::OsStr,
    time::{Duration, Instant},
};

use sysinfo::{
    CpuRefreshKind, DiskRefreshKind, Disks, Networks, Pid, Process, ProcessRefreshKind,
    ProcessStatus, ProcessesToUpdate, Signal, System, UpdateKind, Users,
};

use crate::error::{self, Result};

#[derive(Debug, Clone)]
pub struct Snapshot {
    pub totals: SystemTotals,
    pub processes: Vec<ProcessRow>,
    pub disks: Vec<DiskRow>,
    pub networks: Vec<NetworkRow>,
    pub sample_span: Duration,
    pub process_count: usize,
}

#[derive(Debug, Clone)]
pub struct SystemTotals {
    pub cpu_usage: f32,
    pub cpu_count: usize,
    pub total_memory: u64,
    pub used_memory: u64,
    pub total_swap: u64,
    pub used_swap: u64,
    pub disk_read_rate: f64,
    pub disk_write_rate: f64,
    pub net_in_rate: f64,
    pub net_out_rate: f64,
    pub uptime: u64,
    pub host: String,
    pub os: String,
}

#[derive(Debug, Clone)]
pub struct ProcessRow {
    pub pid: u32,
    pub pid_str: String,
    pub parent_pid: Option<u32>,
    pub name: String,
    pub sort_name: String,
    pub user: String,
    pub user_lowercase: String,
    pub command: String,
    pub command_lowercase: String,
    pub exe: String,
    pub cwd: String,
    pub status: String,
    pub status_lowercase: String,
    pub cpu_usage: f32,
    pub memory: u64,
    pub virtual_memory: u64,
    pub memory_percent: f64,
    pub disk_read_rate: f64,
    pub disk_write_rate: f64,
    pub total_disk_read: u64,
    pub total_disk_write: u64,
    pub run_time: u64,
    pub start_time: u64,
    pub energy_impact: f64,
    pub trend: ProcessTrend,
    pub selected_details: Option<SelectedProcessDetails>,
    pub search_text: String,
}

#[derive(Debug, Clone, Default)]
pub struct ProcessTrend {
    pub cpu_delta: f32,
    pub memory_delta: i64,
    pub disk_read_rate_delta: f64,
    pub disk_write_rate_delta: f64,
    pub new_process: bool,
}

impl ProcessTrend {
    pub fn disk_rate_delta(&self) -> f64 {
        self.disk_read_rate_delta + self.disk_write_rate_delta
    }

    pub fn score(&self) -> f64 {
        let memory_mib = self.memory_delta.unsigned_abs() as f64 / 1_048_576.0;
        let disk_mib = self.disk_rate_delta().abs() / 1_048_576.0;
        self.cpu_delta.abs() as f64 + memory_mib.min(100.0) + disk_mib.min(100.0)
    }

    /// A short human explanation of why this process is a mover, naming the
    /// single dominant driver of change. Returns `None` when nothing changed.
    pub fn headline(&self) -> Option<String> {
        if self.new_process {
            return Some("new process".to_string());
        }

        let cpu_magnitude = self.cpu_delta.abs() as f64;
        let memory_mib = self.memory_delta.unsigned_abs() as f64 / 1_048_576.0;
        let disk_mib = self.disk_rate_delta().abs() / 1_048_576.0;

        if cpu_magnitude.max(memory_mib).max(disk_mib) < 0.05 {
            return None;
        }

        if cpu_magnitude >= memory_mib && cpu_magnitude >= disk_mib {
            Some(format!(
                "CPU {}",
                crate::format::signed_percent(self.cpu_delta as f64)
            ))
        } else if memory_mib >= disk_mib {
            Some(format!(
                "mem {}",
                crate::format::signed_bytes(self.memory_delta)
            ))
        } else {
            Some(format!(
                "disk {}",
                crate::format::signed_bytes_rate(self.disk_rate_delta())
            ))
        }
    }
}

#[derive(Debug, Clone)]
pub struct ProcessSample {
    cpu_usage: f32,
    memory: u64,
    disk_read_rate: f64,
    disk_write_rate: f64,
}

#[derive(Debug, Clone)]
pub struct SelectedProcessDetails {
    pub thread_count: Option<usize>,
    pub open_files: Option<usize>,
    pub open_files_limit: Option<usize>,
    pub session_id: Option<u32>,
    pub priority: Option<i32>,
}

#[derive(Debug, Clone)]
pub struct DiskRow {
    pub name: String,
    pub mount_point: String,
    pub total: u64,
    pub available: u64,
    pub read_rate: f64,
    pub write_rate: f64,
}

#[derive(Debug, Clone)]
pub struct NetworkRow {
    pub name: String,
    pub received_rate: f64,
    pub transmitted_rate: f64,
    pub total_received: u64,
    pub total_transmitted: u64,
}

pub struct Sampler {
    system: System,
    disks: Disks,
    networks: Networks,
    users: Users,
    last_sample: Instant,
    static_info: StaticInfo,
}

#[derive(Debug, Clone)]
struct StaticInfo {
    host: String,
    os: String,
}

impl Sampler {
    pub fn new() -> Result<Self> {
        let mut system = System::new();
        system.refresh_cpu_specifics(CpuRefreshKind::everything());
        system.refresh_memory();
        system.refresh_processes_specifics(ProcessesToUpdate::All, true, process_refresh_kind());

        let disks = Disks::new_with_refreshed_list_specifics(DiskRefreshKind::everything());
        let networks = Networks::new_with_refreshed_list();
        let users = Users::new_with_refreshed_list();

        Ok(Self {
            system,
            disks,
            networks,
            users,
            last_sample: Instant::now(),
            static_info: StaticInfo {
                host: System::host_name().unwrap_or_else(|| "localhost".to_string()),
                os: System::long_os_version()
                    .or_else(System::name)
                    .unwrap_or_else(|| std::env::consts::OS.to_string()),
            },
        })
    }

    pub fn sample(&mut self, detail_pid: Option<u32>) -> Snapshot {
        let now = Instant::now();
        let sample_span = now.saturating_duration_since(self.last_sample);
        self.last_sample = now;
        let seconds = sample_span.as_secs_f64().max(0.25);

        self.system
            .refresh_cpu_specifics(CpuRefreshKind::nothing().with_cpu_usage());
        self.system.refresh_memory();
        self.system.refresh_processes_specifics(
            ProcessesToUpdate::All,
            true,
            process_refresh_kind(),
        );
        self.disks
            .refresh_specifics(true, DiskRefreshKind::everything());
        self.networks.refresh(true);

        let total_memory = self.system.total_memory();
        let processes = self
            .system
            .processes()
            .values()
            .map(|process| self.process_row(process, total_memory, detail_pid, seconds))
            .collect::<Vec<_>>();

        let (disk_read_rate, disk_write_rate, disks) = self.disk_rows(seconds);
        let (net_in_rate, net_out_rate, networks) = self.network_rows(seconds);

        Snapshot {
            totals: SystemTotals {
                cpu_usage: self.system.global_cpu_usage(),
                cpu_count: self.system.cpus().len(),
                total_memory,
                used_memory: self.system.used_memory(),
                total_swap: self.system.total_swap(),
                used_swap: self.system.used_swap(),
                disk_read_rate,
                disk_write_rate,
                net_in_rate,
                net_out_rate,
                uptime: System::uptime(),
                host: self.static_info.host.clone(),
                os: self.static_info.os.clone(),
            },
            process_count: processes.len(),
            processes,
            disks,
            networks,
            sample_span,
        }
    }

    pub fn send_signal(&self, pid: u32, signal: Signal) -> Result<()> {
        let pid = Pid::from_u32(pid);
        let process = self
            .system
            .process(pid)
            .ok_or_else(|| error::message(format!("process {pid} is no longer visible")))?;
        match process.kill_with(signal) {
            Some(true) => Ok(()),
            Some(false) => Err(error::message(format!(
                "failed to send {signal} to pid {pid}"
            ))),
            None => Err(error::message(format!(
                "{signal} is not supported on this platform"
            ))),
        }
    }

    pub fn adjust_priority(&self, pid: u32, delta: i32) -> Result<i32> {
        let sys_pid = Pid::from_u32(pid);
        self.system
            .process(sys_pid)
            .ok_or_else(|| error::message(format!("process {sys_pid} is no longer visible")))?;
        let current = platform::priority(pid)
            .ok_or_else(|| error::message(format!("priority is unavailable for pid {pid}")))?;
        let next = (current + delta).clamp(-20, 20);
        platform::set_priority(pid, next)?;
        Ok(next)
    }

    pub fn selected_process_details(&mut self, pid: u32) -> Option<SelectedProcessDetails> {
        let pid = Pid::from_u32(pid);
        self.system.refresh_processes_specifics(
            ProcessesToUpdate::Some(&[pid]),
            false,
            process_refresh_kind(),
        );
        self.system
            .process(pid)
            .map(|process| SelectedProcessDetails {
                thread_count: platform::thread_count(process.pid().as_u32()),
                open_files: process.open_files(),
                open_files_limit: process.open_files_limit(),
                session_id: process.session_id().map(|pid| pid.as_u32()),
                priority: platform::priority(process.pid().as_u32()),
            })
    }

    fn process_row(
        &self,
        process: &Process,
        total_memory: u64,
        detail_pid: Option<u32>,
        seconds: f64,
    ) -> ProcessRow {
        let disk = process.disk_usage();
        let pid = process.pid().as_u32();
        let name = os_to_string(process.name());
        let sort_name = name.to_lowercase();
        let command = command_line(process);
        let user = process
            .user_id()
            .and_then(|id| self.users.get_user_by_id(id))
            .map(|user| user.name().to_string())
            .unwrap_or_else(|| "-".to_string());
        let exe = process
            .exe()
            .map(|path| path.display().to_string())
            .unwrap_or_else(|| "-".to_string());
        let cwd = process
            .cwd()
            .map(|path| path.display().to_string())
            .unwrap_or_else(|| "-".to_string());
        let memory = process.memory();
        let memory_percent = if total_memory > 0 {
            (memory as f64 / total_memory as f64) * 100.0
        } else {
            0.0
        };
        let disk_read_rate = disk.read_bytes as f64 / seconds;
        let disk_write_rate = disk.written_bytes as f64 / seconds;
        let energy_impact = energy_impact(
            process.cpu_usage(),
            memory_percent,
            disk_read_rate + disk_write_rate,
            process.status(),
        );
        let selected_details = (Some(pid) == detail_pid).then(|| SelectedProcessDetails {
            thread_count: platform::thread_count(pid),
            open_files: process.open_files(),
            open_files_limit: process.open_files_limit(),
            session_id: process.session_id().map(|pid| pid.as_u32()),
            priority: platform::priority(pid),
        });
        let parent_pid = process.parent().map(|pid| pid.as_u32());
        let status = status_label(process.status()).to_string();
        let user_lowercase = user.to_lowercase();
        let command_lowercase = command.to_lowercase();
        let status_lowercase = status.to_lowercase();
        let search_text = format!(
            "{} {} {} {} {}",
            pid, sort_name, user_lowercase, command_lowercase, status_lowercase
        );

        ProcessRow {
            pid,
            pid_str: pid.to_string(),
            parent_pid,
            name,
            sort_name,
            user,
            user_lowercase,
            command,
            command_lowercase,
            exe,
            cwd,
            status,
            status_lowercase,
            cpu_usage: process.cpu_usage(),
            memory,
            virtual_memory: process.virtual_memory(),
            memory_percent,
            disk_read_rate,
            disk_write_rate,
            total_disk_read: disk.total_read_bytes,
            total_disk_write: disk.total_written_bytes,
            run_time: process.run_time(),
            start_time: process.start_time(),
            energy_impact,
            trend: ProcessTrend::default(),
            selected_details,
            search_text,
        }
    }

    fn disk_rows(&self, seconds: f64) -> (f64, f64, Vec<DiskRow>) {
        let mut read_rate = 0.0;
        let mut write_rate = 0.0;
        let rows = self
            .disks
            .list()
            .iter()
            .map(|disk| {
                let usage = disk.usage();
                let row_read = usage.read_bytes as f64 / seconds;
                let row_write = usage.written_bytes as f64 / seconds;
                read_rate += row_read;
                write_rate += row_write;
                DiskRow {
                    name: os_to_string(disk.name()),
                    mount_point: disk.mount_point().display().to_string(),
                    total: disk.total_space(),
                    available: disk.available_space(),
                    read_rate: row_read,
                    write_rate: row_write,
                }
            })
            .collect();
        (read_rate, write_rate, rows)
    }

    fn network_rows(&self, seconds: f64) -> (f64, f64, Vec<NetworkRow>) {
        let mut in_rate = 0.0;
        let mut out_rate = 0.0;
        let rows = self
            .networks
            .iter()
            .map(|(name, data)| {
                let received_rate = data.received() as f64 / seconds;
                let transmitted_rate = data.transmitted() as f64 / seconds;
                in_rate += received_rate;
                out_rate += transmitted_rate;
                NetworkRow {
                    name: name.clone(),
                    received_rate,
                    transmitted_rate,
                    total_received: data.total_received(),
                    total_transmitted: data.total_transmitted(),
                }
            })
            .collect();
        (in_rate, out_rate, rows)
    }
}

pub fn collect_process_samples(processes: &[ProcessRow]) -> HashMap<u32, ProcessSample> {
    processes
        .iter()
        .map(|process| {
            (
                process.pid,
                ProcessSample {
                    cpu_usage: process.cpu_usage,
                    memory: process.memory,
                    disk_read_rate: process.disk_read_rate,
                    disk_write_rate: process.disk_write_rate,
                },
            )
        })
        .collect()
}

pub fn apply_process_trends(processes: &mut [ProcessRow], previous: &HashMap<u32, ProcessSample>) {
    for process in processes {
        process.trend = previous
            .get(&process.pid)
            .map(|sample| ProcessTrend {
                cpu_delta: process.cpu_usage - sample.cpu_usage,
                memory_delta: process.memory as i64 - sample.memory as i64,
                disk_read_rate_delta: process.disk_read_rate - sample.disk_read_rate,
                disk_write_rate_delta: process.disk_write_rate - sample.disk_write_rate,
                new_process: false,
            })
            .unwrap_or_else(|| ProcessTrend {
                new_process: true,
                ..ProcessTrend::default()
            });
    }
}

fn process_refresh_kind() -> ProcessRefreshKind {
    ProcessRefreshKind::nothing()
        .with_cpu()
        .with_memory()
        .with_disk_usage()
        .with_user(UpdateKind::OnlyIfNotSet)
        .with_cmd(UpdateKind::OnlyIfNotSet)
        .with_exe(UpdateKind::OnlyIfNotSet)
        .with_cwd(UpdateKind::OnlyIfNotSet)
        .without_tasks()
}

fn command_line(process: &Process) -> String {
    if process.cmd().is_empty() {
        return os_to_string(process.name());
    }
    process
        .cmd()
        .iter()
        .map(|part| part.to_string_lossy())
        .collect::<Vec<_>>()
        .join(" ")
}

fn os_to_string(value: &OsStr) -> String {
    let value = value.to_string_lossy();
    if value.is_empty() {
        "-".to_string()
    } else {
        value.into_owned()
    }
}

fn status_label(status: ProcessStatus) -> &'static str {
    match status {
        ProcessStatus::Idle => "idle",
        ProcessStatus::Run => "running",
        ProcessStatus::Sleep => "sleeping",
        ProcessStatus::Stop => "stopped",
        ProcessStatus::Zombie => "zombie",
        ProcessStatus::Tracing => "tracing",
        ProcessStatus::Dead => "dead",
        ProcessStatus::Wakekill => "wakekill",
        ProcessStatus::Waking => "waking",
        ProcessStatus::Parked => "parked",
        ProcessStatus::LockBlocked => "blocked",
        ProcessStatus::UninterruptibleDiskSleep => "disk sleep",
        ProcessStatus::Suspended => "suspended",
        ProcessStatus::Unknown(_) => "unknown",
    }
}

fn energy_impact(cpu: f32, memory_percent: f64, io_rate: f64, status: ProcessStatus) -> f64 {
    let activity = match status {
        ProcessStatus::Run => 1.0,
        ProcessStatus::Sleep | ProcessStatus::Idle => 0.0,
        _ => 0.25,
    };
    let io_mib = io_rate / 1_048_576.0;
    (cpu as f64 * 0.75) + (memory_percent * 0.2) + (io_mib * 0.35) + activity
}

mod platform {
    #[cfg(target_os = "macos")]
    pub fn thread_count(pid: u32) -> Option<usize> {
        use std::{ffi::c_void, mem::size_of};

        const PROC_PIDTASKINFO: libc::c_int = 4;

        #[repr(C)]
        #[derive(Default)]
        struct ProcTaskInfo {
            pti_virtual_size: u64,
            pti_resident_size: u64,
            pti_total_user: u64,
            pti_total_system: u64,
            pti_threads_user: u64,
            pti_threads_system: u64,
            pti_policy: i32,
            pti_faults: i32,
            pti_pageins: i32,
            pti_cow_faults: i32,
            pti_messages_sent: i32,
            pti_messages_received: i32,
            pti_syscalls_mach: i32,
            pti_syscalls_unix: i32,
            pti_csw: i32,
            pti_threadnum: i32,
            pti_numrunning: i32,
            pti_priority: i32,
        }

        unsafe extern "C" {
            fn proc_pidinfo(
                pid: libc::c_int,
                flavor: libc::c_int,
                arg: u64,
                buffer: *mut c_void,
                buffersize: libc::c_int,
            ) -> libc::c_int;
        }

        let mut info = ProcTaskInfo::default();
        let size = size_of::<ProcTaskInfo>() as libc::c_int;
        let written = unsafe {
            proc_pidinfo(
                pid as libc::c_int,
                PROC_PIDTASKINFO,
                0,
                &mut info as *mut _ as *mut c_void,
                size,
            )
        };

        (written == size && info.pti_threadnum >= 0).then_some(info.pti_threadnum as usize)
    }

    #[cfg(target_os = "macos")]
    pub fn priority(pid: u32) -> Option<i32> {
        unsafe {
            *libc::__error() = 0;
            let value = libc::getpriority(libc::PRIO_PROCESS, pid as libc::id_t);
            (*libc::__error() == 0).then_some(value)
        }
    }

    #[cfg(target_os = "macos")]
    pub fn set_priority(pid: u32, priority: i32) -> std::io::Result<()> {
        let result = unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, priority) };
        if result == 0 {
            Ok(())
        } else {
            Err(std::io::Error::last_os_error())
        }
    }

    #[cfg(not(target_os = "macos"))]
    pub fn thread_count(_pid: u32) -> Option<usize> {
        None
    }

    #[cfg(not(target_os = "macos"))]
    pub fn priority(_pid: u32) -> Option<i32> {
        None
    }

    #[cfg(not(target_os = "macos"))]
    pub fn set_priority(_pid: u32, _priority: i32) -> std::io::Result<()> {
        Err(std::io::Error::other(
            "priority control is not supported on this platform",
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::{ProcessRow, ProcessTrend, apply_process_trends, collect_process_samples};

    #[test]
    fn process_trends_compare_against_previous_sample() {
        let previous = vec![process(1, 10.0, 1_000, 100.0, 200.0)];
        let previous = collect_process_samples(&previous);
        let mut current = vec![
            process(1, 25.0, 1_500, 250.0, 150.0),
            process(2, 5.0, 500, 0.0, 0.0),
        ];

        apply_process_trends(&mut current, &previous);

        assert_eq!(current[0].trend.cpu_delta, 15.0);
        assert_eq!(current[0].trend.memory_delta, 500);
        assert_eq!(current[0].trend.disk_read_rate_delta, 150.0);
        assert_eq!(current[0].trend.disk_write_rate_delta, -50.0);
        assert!(!current[0].trend.new_process);
        assert!(current[1].trend.new_process);
    }

    #[test]
    fn trend_headline_names_the_dominant_driver() {
        let new_process = ProcessTrend {
            new_process: true,
            ..ProcessTrend::default()
        };
        assert_eq!(new_process.headline().as_deref(), Some("new process"));

        let cpu_heavy = ProcessTrend {
            cpu_delta: 35.0,
            memory_delta: 1_048_576,
            ..ProcessTrend::default()
        };
        assert_eq!(cpu_heavy.headline().as_deref(), Some("CPU +35.0%"));

        let memory_heavy = ProcessTrend {
            cpu_delta: 1.0,
            memory_delta: 200_000_000,
            ..ProcessTrend::default()
        };
        assert_eq!(memory_heavy.headline().as_deref(), Some("mem +200 MB"));

        let idle = ProcessTrend::default();
        assert_eq!(idle.headline(), None);
    }

    fn process(
        pid: u32,
        cpu_usage: f32,
        memory: u64,
        disk_read_rate: f64,
        disk_write_rate: f64,
    ) -> ProcessRow {
        ProcessRow {
            pid,
            pid_str: pid.to_string(),
            parent_pid: None,
            name: format!("process-{pid}"),
            sort_name: format!("process-{pid}"),
            user: "user".to_string(),
            user_lowercase: "user".to_string(),
            command: "command".to_string(),
            command_lowercase: "command".to_string(),
            exe: "-".to_string(),
            cwd: "-".to_string(),
            status: "running".to_string(),
            status_lowercase: "running".to_string(),
            cpu_usage,
            memory,
            virtual_memory: memory,
            memory_percent: 0.0,
            disk_read_rate,
            disk_write_rate,
            total_disk_read: 0,
            total_disk_write: 0,
            run_time: 0,
            start_time: 0,
            energy_impact: 0.0,
            trend: ProcessTrend::default(),
            selected_details: None,
            search_text: String::new(),
        }
    }
}
