use std::collections::HashMap;
use std::fs;
use std::io::{self, Read};
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::sync::Arc;

pub const CMDLINE_LIMIT: usize = 16 * 1024;
pub const PROC_STAT_BYTES_LIMIT: usize = 512 * 1024;
pub const PROC_MEMINFO_BYTES_LIMIT: usize = 64 * 1024;
pub const PROC_LOADAVG_BYTES_LIMIT: usize = 1024;
pub const PROC_UPTIME_BYTES_LIMIT: usize = 1024;
pub const PROC_PID_STAT_BYTES_LIMIT: usize = 4096;
pub const PASSWD_BYTES_LIMIT: usize = 1024 * 1024;
pub const USER_RETAIN_LIMIT: usize = 128;
pub const PROCESS_NAME_RETAIN_LIMIT: usize = 256;
pub const CMD_RETAIN_LIMIT: usize = 4096;
pub const SEARCH_TEXT_RETAIN_LIMIT: usize = 8192;
pub const PROCESS_SAMPLE_LIMIT: usize = 16_384;
pub const CPU_SAMPLE_LIMIT: usize = 4096;
pub const USER_CACHE_LIMIT: usize = 8192;

#[derive(Clone, Debug, Default)]
pub struct SystemView {
    pub cpu_total: f32,
    pub cpus: Vec<CpuCore>,
    pub memory: MemInfo,
    pub load: LoadAverage,
    pub states: StateCounts,
    pub limits: SampleLimits,
    pub processes: Vec<ProcessInfo>,
    pub thread_count: u64,
}

#[derive(Clone, Debug, Default)]
pub struct CpuCore {
    pub name: String,
    pub usage: f32,
}

#[derive(Clone, Debug, Default)]
pub struct MemInfo {
    pub total: u64,
    pub available: u64,
    pub used: u64,
    pub swap_total: u64,
    pub swap_used: u64,
}

#[derive(Clone, Copy, Debug, Default)]
pub struct LoadAverage {
    pub one: f32,
    pub five: f32,
    pub fifteen: f32,
}

#[derive(Clone, Copy, Debug, Default)]
pub struct SampleLimits {
    pub process_limit_hit: bool,
    pub cpu_limit_hit: bool,
}

#[derive(Clone, Copy, Debug, Default)]
pub struct StateCounts {
    pub running: u64,
    pub sleeping: u64,
    pub disk_sleep: u64,
    pub stopped: u64,
    pub zombie: u64,
    pub idle: u64,
    pub other: u64,
}

impl StateCounts {
    fn record(&mut self, state: char) {
        match state {
            'R' => self.running += 1,
            'S' => self.sleeping += 1,
            'D' => self.disk_sleep += 1,
            'T' | 't' => self.stopped += 1,
            'Z' => self.zombie += 1,
            'I' => self.idle += 1,
            _ => self.other += 1,
        }
    }
}

#[derive(Clone, Debug)]
pub struct ProcessInfo {
    pub pid: u32,
    pub ppid: u32,
    pub uid: u32,
    pub user: Arc<str>,
    pub name: Arc<str>,
    pub cmd: Arc<str>,
    pub user_lower: Arc<str>,
    pub name_lower: Arc<str>,
    pub cmd_lower: Arc<str>,
    pub search_text: Arc<str>,
    pub state: char,
    pub cpu: f32,
    pub mem: f32,
    pub rss: u64,
    pub vms: u64,
    pub threads: u32,
    pub runtime: u64,
    pub started_after_boot: u64,
    start_time: u64,
    ticks: u64,
}

#[derive(Clone, Debug)]
struct ProcessMeta {
    start_time: u64,
    uid: u32,
    user: Arc<str>,
    name: Arc<str>,
    cmd: Arc<str>,
    user_lower: Arc<str>,
    name_lower: Arc<str>,
    cmd_lower: Arc<str>,
    search_text: Arc<str>,
}

#[derive(Clone, Copy, Debug, Default)]
struct CpuTimes {
    user: u64,
    nice: u64,
    system: u64,
    idle: u64,
    iowait: u64,
    irq: u64,
    softirq: u64,
    steal: u64,
    guest: u64,
    guest_nice: u64,
}

impl CpuTimes {
    fn total(self) -> u64 {
        self.user
            + self.nice
            + self.system
            + self.idle
            + self.iowait
            + self.irq
            + self.softirq
            + self.steal
            + self.guest
            + self.guest_nice
    }

    fn idle_all(self) -> u64 {
        self.idle + self.iowait
    }
}

pub struct Sampler {
    previous_cpu: Vec<(String, CpuTimes)>,
    previous_proc_ticks: HashMap<u32, (u64, u64)>,
    metadata: HashMap<u32, ProcessMeta>,
    users: HashMap<u32, String>,
    page_size: u64,
    ticks_per_second: u64,
}

impl Sampler {
    pub fn new() -> Self {
        Self {
            previous_cpu: Vec::new(),
            previous_proc_ticks: HashMap::new(),
            metadata: HashMap::new(),
            users: load_users(),
            page_size: page_size(),
            ticks_per_second: clock_ticks_per_second(),
        }
    }

    pub fn sample(&mut self) -> io::Result<SystemView> {
        let memory = read_memory()?;
        let load = read_loadavg().unwrap_or_default();
        let uptime = read_uptime().unwrap_or(0.0);
        let (cpu_times, cpu_limit_hit) = read_cpu_times()?;
        let cpu_total = usage_between(self.previous_cpu.first(), cpu_times.first());
        let cpus = cpu_times
            .iter()
            .enumerate()
            .skip(1)
            .map(|(index, (name, times))| {
                let label = name
                    .strip_prefix("cpu")
                    .filter(|label| !label.is_empty())
                    .map(str::to_string)
                    .unwrap_or_else(|| index.to_string());
                CpuCore {
                    name: label,
                    usage: usage_between(
                        self.previous_cpu.get(index),
                        Some(&(name.clone(), *times)),
                    ),
                }
            })
            .collect::<Vec<_>>();

        let total_delta = match (self.previous_cpu.first(), cpu_times.first()) {
            (Some((_, previous)), Some((_, current))) => {
                current.total().saturating_sub(previous.total())
            }
            _ => 0,
        };
        let cores = cpus.len().max(1) as f32;
        let cpu_window = if total_delta > 0 {
            total_delta as f32 / cores
        } else {
            0.0
        };

        let mut next_proc_ticks =
            HashMap::with_capacity(self.previous_proc_ticks.len().min(PROCESS_SAMPLE_LIMIT));
        let mut processes =
            Vec::with_capacity(self.previous_proc_ticks.len().min(PROCESS_SAMPLE_LIMIT));
        let mut thread_count = 0_u64;
        let mut states = StateCounts::default();
        let mut process_limit_hit = false;

        for entry in fs::read_dir("/proc")? {
            if processes.len() >= PROCESS_SAMPLE_LIMIT {
                process_limit_hit = true;
                break;
            }

            let entry = match entry {
                Ok(entry) => entry,
                Err(_) => continue,
            };
            let file_name = entry.file_name();
            let Some(pid) = file_name.to_string_lossy().parse::<u32>().ok() else {
                continue;
            };

            let Some(mut process) = self.read_process(pid, memory.total, uptime) else {
                continue;
            };

            if let Some((previous_start, previous_ticks)) = self.previous_proc_ticks.get(&pid) {
                if *previous_start == process.start_time {
                    let delta = process.ticks.saturating_sub(*previous_ticks);
                    process.cpu = if cpu_window > 0.0 {
                        (delta as f32 / cpu_window) * 100.0
                    } else {
                        0.0
                    };
                }
            }

            thread_count += process.threads as u64;
            states.record(process.state);
            next_proc_ticks.insert(pid, (process.start_time, process.ticks));
            processes.push(process);
            if processes.len() >= PROCESS_SAMPLE_LIMIT {
                process_limit_hit = true;
            }
        }

        self.previous_cpu = cpu_times;
        self.previous_proc_ticks = next_proc_ticks;
        self.metadata
            .retain(|pid, meta| match self.previous_proc_ticks.get(pid) {
                Some((start_time, _)) => *start_time == meta.start_time,
                None => false,
            });

        Ok(SystemView {
            cpu_total,
            cpus,
            memory,
            load,
            states,
            limits: SampleLimits {
                process_limit_hit,
                cpu_limit_hit,
            },
            processes,
            thread_count,
        })
    }

    pub fn cached_processes(&self) -> usize {
        self.metadata.len()
    }

    pub fn cached_users(&self) -> usize {
        self.users.len()
    }

    fn read_process(&mut self, pid: u32, total_memory: u64, uptime: f64) -> Option<ProcessInfo> {
        let stat =
            read_to_string_limited(format!("/proc/{pid}/stat"), PROC_PID_STAT_BYTES_LIMIT).ok()?;
        let parsed = parse_stat(&stat)?;

        let refresh_meta = match self.metadata.get(&pid) {
            Some(meta) => meta.start_time != parsed.start_time,
            None => true,
        };
        if refresh_meta {
            let meta = read_process_meta(
                pid,
                parsed.start_time,
                parsed.name,
                parsed.ppid,
                &self.users,
            );
            self.metadata.insert(pid, meta);
        }
        let meta = self.metadata.get(&pid)?;

        let rss = parsed.rss_pages.saturating_mul(self.page_size);
        let mem = if total_memory > 0 {
            (rss as f32 / total_memory as f32) * 100.0
        } else {
            0.0
        };
        let started_after_boot = parsed.start_time / self.ticks_per_second.max(1);
        let runtime =
            uptime.max(0.0).round().max(started_after_boot as f64) as u64 - started_after_boot;
        Some(ProcessInfo {
            pid,
            ppid: parsed.ppid,
            uid: meta.uid,
            user: meta.user.clone(),
            name: meta.name.clone(),
            cmd: meta.cmd.clone(),
            user_lower: meta.user_lower.clone(),
            name_lower: meta.name_lower.clone(),
            cmd_lower: meta.cmd_lower.clone(),
            search_text: meta.search_text.clone(),
            state: parsed.state,
            cpu: 0.0,
            mem,
            rss,
            vms: parsed.vms,
            threads: parsed.threads,
            runtime,
            started_after_boot,
            start_time: parsed.start_time,
            ticks: parsed.ticks,
        })
    }
}

fn usage_between(
    previous: Option<&(String, CpuTimes)>,
    current: Option<&(String, CpuTimes)>,
) -> f32 {
    let (Some((_, previous)), Some((_, current))) = (previous, current) else {
        return 0.0;
    };
    let total = current.total().saturating_sub(previous.total());
    if total == 0 {
        return 0.0;
    }
    let idle = current.idle_all().saturating_sub(previous.idle_all());
    ((total.saturating_sub(idle)) as f32 / total as f32) * 100.0
}

fn read_cpu_times() -> io::Result<(Vec<(String, CpuTimes)>, bool)> {
    let stat = read_to_string_limited("/proc/stat", PROC_STAT_BYTES_LIMIT)?;
    let mut cpus = Vec::with_capacity(64);
    let mut limit_hit = false;

    for line in stat.lines() {
        if !line.starts_with("cpu") {
            break;
        }
        if cpus.len() >= CPU_SAMPLE_LIMIT {
            limit_hit = true;
            break;
        }
        let mut parts = line.split_whitespace();
        let Some(name) = parts.next() else {
            continue;
        };
        cpus.push((
            name.to_string(),
            CpuTimes {
                user: parse_next_u64(&mut parts),
                nice: parse_next_u64(&mut parts),
                system: parse_next_u64(&mut parts),
                idle: parse_next_u64(&mut parts),
                iowait: parse_next_u64(&mut parts),
                irq: parse_next_u64(&mut parts),
                softirq: parse_next_u64(&mut parts),
                steal: parse_next_u64(&mut parts),
                guest: parse_next_u64(&mut parts),
                guest_nice: parse_next_u64(&mut parts),
            },
        ));
        if cpus.len() >= CPU_SAMPLE_LIMIT {
            limit_hit = true;
        }
    }

    Ok((cpus, limit_hit))
}

fn read_memory() -> io::Result<MemInfo> {
    let meminfo = read_to_string_limited("/proc/meminfo", PROC_MEMINFO_BYTES_LIMIT)?;
    let mut total = 0;
    let mut available = 0;
    let mut swap_total = 0;
    let mut swap_free = 0;

    for line in meminfo.lines() {
        let mut parts = line.split_whitespace();
        let Some(key) = parts.next() else {
            continue;
        };
        let value = parts
            .next()
            .and_then(|part| part.parse::<u64>().ok())
            .unwrap_or(0)
            * 1024;

        match key {
            "MemTotal:" => total = value,
            "MemAvailable:" => available = value,
            "SwapTotal:" => swap_total = value,
            "SwapFree:" => swap_free = value,
            _ => {}
        }
    }

    let used = total.saturating_sub(available);

    Ok(MemInfo {
        total,
        available,
        used,
        swap_total,
        swap_used: swap_total.saturating_sub(swap_free),
    })
}

fn read_loadavg() -> io::Result<LoadAverage> {
    let loadavg = read_to_string_limited("/proc/loadavg", PROC_LOADAVG_BYTES_LIMIT)?;
    let mut parts = loadavg.split_whitespace();

    Ok(LoadAverage {
        one: parts
            .next()
            .and_then(|part| part.parse().ok())
            .unwrap_or(0.0),
        five: parts
            .next()
            .and_then(|part| part.parse().ok())
            .unwrap_or(0.0),
        fifteen: parts
            .next()
            .and_then(|part| part.parse().ok())
            .unwrap_or(0.0),
    })
}

fn read_uptime() -> io::Result<f64> {
    let uptime = read_to_string_limited("/proc/uptime", PROC_UPTIME_BYTES_LIMIT)?;
    Ok(uptime
        .split_whitespace()
        .next()
        .and_then(|part| part.parse::<f64>().ok())
        .unwrap_or(0.0))
}

fn clock_ticks_per_second() -> u64 {
    let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
    if ticks > 0 {
        ticks as u64
    } else {
        100
    }
}

fn page_size() -> u64 {
    let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
    if size > 0 {
        size as u64
    } else {
        4096
    }
}

struct ParsedStat<'a> {
    name: &'a str,
    state: char,
    ppid: u32,
    ticks: u64,
    threads: u32,
    start_time: u64,
    vms: u64,
    rss_pages: u64,
}

fn parse_stat(stat: &str) -> Option<ParsedStat<'_>> {
    let open = stat.find('(')?;
    let close = stat.rfind(')')?;
    let name = stat.get(open + 1..close)?;
    let mut fields = stat.get(close + 2..)?.split_whitespace();

    let state = fields.next()?.chars().next().unwrap_or('?');
    let ppid = fields.next()?.parse::<u32>().ok()?;
    for _ in 0..9 {
        fields.next()?;
    }
    let utime = fields.next()?.parse::<u64>().ok()?;
    let stime = fields.next()?.parse::<u64>().ok()?;
    for _ in 0..4 {
        fields.next()?;
    }
    let threads = fields.next()?.parse::<u32>().unwrap_or(1);
    fields.next()?;
    let start_time = fields.next()?.parse::<u64>().ok()?;
    let vms = fields.next()?.parse::<u64>().unwrap_or(0);
    let rss_pages = fields.next()?.parse::<i64>().unwrap_or(0).max(0) as u64;

    Some(ParsedStat {
        name,
        state,
        ppid,
        ticks: utime.saturating_add(stime),
        threads,
        start_time,
        vms,
        rss_pages,
    })
}

fn read_process_meta(
    pid: u32,
    start_time: u64,
    name: &str,
    ppid: u32,
    users: &HashMap<u32, String>,
) -> ProcessMeta {
    let uid = read_uid(pid).unwrap_or(0);
    let user = truncate_to_bytes(
        &users.get(&uid).cloned().unwrap_or_else(|| uid.to_string()),
        USER_RETAIN_LIMIT,
    );
    let name = truncate_to_bytes(name, PROCESS_NAME_RETAIN_LIMIT);
    let cmd = read_cmdline(pid)
        .map(|cmd| truncate_to_bytes(&cmd, CMD_RETAIN_LIMIT))
        .unwrap_or_else(|| format!("[{name}]"));
    let user_lower = truncate_to_bytes(&user.to_ascii_lowercase(), USER_RETAIN_LIMIT);
    let name_lower = truncate_to_bytes(&name.to_ascii_lowercase(), PROCESS_NAME_RETAIN_LIMIT);
    let cmd_lower = truncate_to_bytes(&cmd.to_ascii_lowercase(), CMD_RETAIN_LIMIT);
    let search_text = truncate_to_bytes(
        &format!("{pid} {ppid} {uid} {user} {name} {cmd}").to_ascii_lowercase(),
        SEARCH_TEXT_RETAIN_LIMIT,
    );

    ProcessMeta {
        start_time,
        uid,
        user: Arc::from(user),
        name: Arc::from(name),
        cmd: Arc::from(cmd),
        user_lower: Arc::from(user_lower),
        name_lower: Arc::from(name_lower),
        cmd_lower: Arc::from(cmd_lower),
        search_text: Arc::from(search_text),
    }
}

fn read_uid(pid: u32) -> Option<u32> {
    Some(fs::metadata(format!("/proc/{pid}")).ok()?.uid())
}

fn read_to_string_limited(path: impl AsRef<Path>, limit: usize) -> io::Result<String> {
    let mut file = fs::File::open(path)?;
    let mut text = String::new();
    file.by_ref().take(limit as u64).read_to_string(&mut text)?;
    Ok(text)
}

fn truncate_to_bytes(input: &str, limit: usize) -> String {
    if input.len() <= limit {
        return input.to_string();
    }

    let mut end = limit;
    while end > 0 && !input.is_char_boundary(end) {
        end -= 1;
    }
    input[..end].to_string()
}

fn read_cmdline(pid: u32) -> Option<String> {
    let mut file = fs::File::open(format!("/proc/{pid}/cmdline")).ok()?;
    let mut bytes = Vec::with_capacity(256);
    file.by_ref()
        .take(CMDLINE_LIMIT as u64)
        .read_to_end(&mut bytes)
        .ok()?;
    let mut cmd = String::new();
    for part in bytes
        .split(|byte| *byte == 0)
        .filter(|part| !part.is_empty())
    {
        if !cmd.is_empty() {
            cmd.push(' ');
        }
        cmd.push_str(&String::from_utf8_lossy(part));
    }
    if cmd.is_empty() {
        None
    } else {
        Some(cmd)
    }
}

fn load_users() -> HashMap<u32, String> {
    let mut users = HashMap::new();
    let Ok(mut file) = fs::File::open(Path::new("/etc/passwd")) else {
        return users;
    };
    let mut passwd = String::new();
    if file
        .by_ref()
        .take(PASSWD_BYTES_LIMIT as u64)
        .read_to_string(&mut passwd)
        .is_err()
    {
        return users;
    }

    for line in passwd.lines().take(USER_CACHE_LIMIT) {
        let mut fields = line.split(':');
        let Some(name) = fields.next() else {
            continue;
        };
        let Some(uid) = fields.nth(1).and_then(|field| field.parse::<u32>().ok()) else {
            continue;
        };
        if users.len() < USER_CACHE_LIMIT {
            users.insert(uid, name.to_string());
        }
    }

    users
}

fn parse_next_u64(parts: &mut std::str::SplitWhitespace<'_>) -> u64 {
    parts
        .next()
        .and_then(|part| part.parse::<u64>().ok())
        .unwrap_or(0)
}

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

    #[test]
    fn limited_read_caps_text_bytes() {
        let path =
            std::env::temp_dir().join(format!("ktop-limited-read-{}-text", std::process::id()));
        fs::write(&path, "abcdef").unwrap();

        let text = read_to_string_limited(&path, 3).unwrap();

        let _ = fs::remove_file(&path);
        assert_eq!(text, "abc");
    }

    #[test]
    fn limited_read_allows_exact_size() {
        let path =
            std::env::temp_dir().join(format!("ktop-limited-read-{}-exact", std::process::id()));
        fs::write(&path, "abcdef").unwrap();

        let text = read_to_string_limited(&path, 6).unwrap();

        let _ = fs::remove_file(&path);
        assert_eq!(text, "abcdef");
    }

    #[test]
    fn retained_text_truncates_on_utf8_boundary() {
        assert_eq!(truncate_to_bytes("abcdef", 4), "abcd");
        assert_eq!(truncate_to_bytes("ééé", 3), "é");
    }

    #[test]
    fn retained_text_allows_exact_size() {
        assert_eq!(truncate_to_bytes("abcd", 4), "abcd");
    }
}
