//! The machine a measurement ran on (`extensions/staged-measurement.md` §7.4, §11.1):
//! its processor, cores, operating system and architecture, the build's profile, its
//! load, and its power source, as the operating system reports them. Native only: a
//! browser reports none of these.
//!
//! Every reading is what the OS says, unparsed beyond trimming; a reading the OS does
//! not give is `None` (or "unknown"), never estimated.

/// The machine and build a measurement ran on.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MachineRecord {
    /// The processor's name as the OS reports it, or `unknown CPU`.
    pub cpu: String,
    /// `N logical cores`, followed, where the OS reports performance levels, by each
    /// level's core count and name in parentheses.
    pub cores: String,
    pub os: &'static str,
    pub arch: &'static str,
    /// `release`, or `DEBUG (numbers not meaningful)` for a build with debug assertions.
    pub build: &'static str,
}

impl std::fmt::Display for MachineRecord {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}, {}; {}-{}; {} build", self.cpu, self.cores, self.os, self.arch, self.build)
    }
}

fn sh(cmd: &str, args: &[&str]) -> Option<String> {
    let o = std::process::Command::new(cmd).args(args).output().ok()?;
    o.status.success().then(|| String::from_utf8_lossy(&o.stdout).trim().to_string())
}

/// The core types as the OS reports them, e.g. `6 Super + 12 Performance`: each macOS performance
/// level's logical-core count and its own name (`hw.perflevelN.name`), in the OS's
/// order. Names are reported as given, not inferred from the level's index, since what
/// level 0 and 1 are differs between chips. `None` where the OS does not report levels.
fn core_levels() -> Option<String> {
    let n: usize = sh("sysctl", &["-n", "hw.nperflevels"])?.parse().ok()?;
    let parts: Vec<String> = (0..n)
        .map(|i| {
            let cpus = sh("sysctl", &["-n", &format!("hw.perflevel{i}.logicalcpu")])?;
            let name = sh("sysctl", &["-n", &format!("hw.perflevel{i}.name")]).unwrap_or_else(|| format!("level-{i}"));
            Some(format!("{cpus} {name}"))
        })
        .collect::<Option<_>>()?;
    (!parts.is_empty()).then(|| parts.join(" + "))
}

/// This machine and build.
pub fn machine() -> MachineRecord {
    let cpu = sh("sysctl", &["-n", "machdep.cpu.brand_string"])
        .or_else(|| {
            std::fs::read_to_string("/proc/cpuinfo").ok()?.lines().find(|l| l.starts_with("model name")).map(|l| l.split(':').nth(1).unwrap_or("").trim().to_string())
        })
        .unwrap_or_else(|| "unknown CPU".into());
    let levels = core_levels().map(|l| format!(" ({l})")).unwrap_or_default();
    MachineRecord {
        cpu,
        cores: format!("{} logical cores{levels}", std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0)),
        os: std::env::consts::OS,
        arch: std::env::consts::ARCH,
        build: if cfg!(debug_assertions) { "DEBUG (numbers not meaningful)" } else { "release" },
    }
}

/// The 1-, 5- and 15-minute load averages, as the OS writes them.
pub fn loadavg() -> Option<(String, String, String)> {
    let line = sh("sysctl", &["-n", "vm.loadavg"])
        .map(|s| s.trim_matches(|c| c == '{' || c == '}' || c == ' ').to_string())
        .or_else(|| std::fs::read_to_string("/proc/loadavg").ok())?;
    let mut it = line.split_whitespace().map(str::to_string);
    Some((it.next()?, it.next()?, it.next()?))
}

/// Where the machine draws power from, as the OS names it: macOS's `pmset` line
/// (`AC Power`, `Battery Power`), or on Linux `AC Power` when a mains supply is
/// online and `Battery Power` when only a battery reports. `None` when the OS says
/// nothing.
pub fn power_source() -> Option<String> {
    if let Some(out) = sh("pmset", &["-g", "batt"]) {
        let first = out.lines().next()?;
        let (_, rest) = first.split_once('\'')?;
        let (name, _) = rest.split_once('\'')?;
        return Some(name.to_string());
    }
    let dir = std::fs::read_dir("/sys/class/power_supply").ok()?;
    let mut battery = false;
    for e in dir.flatten() {
        let p = e.path();
        let kind = std::fs::read_to_string(p.join("type")).unwrap_or_default();
        match kind.trim() {
            "Mains" if std::fs::read_to_string(p.join("online")).is_ok_and(|s| s.trim() == "1") => return Some("AC Power".into()),
            "Battery" => battery = true,
            _ => {}
        }
    }
    battery.then(|| "Battery Power".into())
}

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

    #[test]
    fn the_record_prints_as_the_decode_bench_printed_it() {
        let m = MachineRecord { cpu: "X".into(), cores: "8 logical cores (4 A + 4 B)".into(), os: "macos", arch: "aarch64", build: "release" };
        assert_eq!(m.to_string(), "X, 8 logical cores (4 A + 4 B); macos-aarch64; release build");
        // On this machine: the record reads, and it names this OS and architecture.
        let here = machine();
        assert_eq!((here.os, here.arch), (std::env::consts::OS, std::env::consts::ARCH));
        assert!(here.cores.ends_with(')') || here.cores.ends_with("cores"), "{}", here.cores);
    }
}
