Documentation
// cpu topology detection and physical core management

#[cfg(target_os = "linux")]
use crate::affinity::{cpu_count, for_each_online_cpu};
use crate::error::CpuAffinityError;
#[cfg(target_os = "linux")]
use std::{collections::HashSet, fs};

#[cfg(target_os = "linux")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct PhysicalCore {
    package_id: usize,
    core_id: usize,
}

// get number of physical cpu cores (excluding hyperthreads)
#[cfg(target_os = "linux")]
pub fn physical_core_count() -> Result<usize, CpuAffinityError> {
    let mut seen_cores = HashSet::new();

    for_each_online_cpu(|cpu| {
        if let Some(core) = physical_core_for_cpu(cpu) {
            seen_cores.insert(core);
        }
    })?;

    if seen_cores.is_empty() {
        // fallback: assume no hyperthreading
        cpu_count()
    } else {
        Ok(seen_cores.len())
    }
}

#[cfg(not(target_os = "linux"))]
pub fn physical_core_count() -> Result<usize, CpuAffinityError> {
    Err(CpuAffinityError::NotSupported)
}

#[cfg(target_os = "linux")]
fn physical_core_for_cpu(cpu: usize) -> Option<PhysicalCore> {
    physical_core_from_ids(
        read_topology_id(cpu, "physical_package_id"),
        read_topology_id(cpu, "core_id"),
    )
}

#[cfg(target_os = "linux")]
fn read_topology_id(cpu: usize, name: &str) -> Option<usize> {
    let path = format!("/sys/devices/system/cpu/cpu{cpu}/topology/{name}");
    fs::read_to_string(path).ok()?.trim().parse().ok()
}

#[cfg(target_os = "linux")]
fn physical_core_from_ids(
    package_id: Option<usize>,
    core_id: Option<usize>,
) -> Option<PhysicalCore> {
    Some(PhysicalCore {
        package_id: package_id.unwrap_or(0),
        core_id: core_id?,
    })
}

#[cfg(test)]
#[cfg(target_os = "linux")]
mod tests {
    use super::{PhysicalCore, physical_core_from_ids};
    use std::collections::HashSet;

    #[test]
    fn physical_core_requires_core_id() {
        assert_eq!(physical_core_from_ids(Some(0), None), None);
    }

    #[test]
    fn physical_core_defaults_missing_package_to_zero() {
        assert_eq!(
            physical_core_from_ids(None, Some(7)),
            Some(PhysicalCore {
                package_id: 0,
                core_id: 7
            })
        );
    }

    #[test]
    fn physical_core_key_includes_package_id() {
        let mut cores = HashSet::new();
        cores.insert(physical_core_from_ids(Some(0), Some(3)).unwrap());
        cores.insert(physical_core_from_ids(Some(1), Some(3)).unwrap());

        assert_eq!(cores.len(), 2);
    }
}