//! OS-level child process health samples to complement ACP JSON silence timeouts.
//!
//! Policy ([`evaluate_after_acp_silence`]) is platform-agnostic; [`sample_child_health`] is
//! OS-specific.
#![allow(dead_code)]

use std::time::{Duration, Instant};

/// Normalized snapshot from the OS for one child PID.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChildHealth {
    pub exists: bool,
    pub zombie: bool,
    pub state_hint: Option<char>,
    /// When `false`, CPU/context/thread fields are placeholders (I/O or parse failure); they must not
    /// be compared to a prior sample as if they were real OS counters.
    pub counters_trusted: bool,
    pub cpu_time_total: u64,
    pub thread_count: Option<u32>,
    pub voluntary_ctxt: Option<u64>,
    pub sample_time: Instant,
}

/// Outcome after the silence window fired once and we took one or two OS health samples.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SilenceHealthOutcome {
    ChildNotRunning,
    ChildZombie,
    StillBusyExtendWait,
    AppearsHung,
}

#[must_use]
pub fn sample_child_health(pid: u32) -> ChildHealth {
    if pid == 0 {
        return ChildHealth::cannot_sample();
    }
    #[cfg(target_os = "linux")]
    {
        linux::sample_child_health_linux(pid)
    }
    #[cfg(target_os = "macos")]
    {
        macos::sample_child_health_macos(pid)
    }
    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    {
        other::sample_child_health_other(pid)
    }
}

#[must_use]
pub fn silence_grace_for_rpc_timeout(rpc_timeout: Duration) -> Duration {
    const MIN: Duration = Duration::from_millis(50);
    const MAX: Duration = Duration::from_millis(250);
    let q = rpc_timeout / 8;
    if q < MIN {
        MIN
    } else if q > MAX {
        MAX
    } else {
        q
    }
}

/// True when two samples suggest the process is still doing work without ACP JSON traffic.
#[must_use]
pub const fn health_indicates_progress(before: &ChildHealth, after: &ChildHealth) -> bool {
    if !after.counters_trusted {
        return false;
    }
    if !before.counters_trusted {
        // Without trusted baseline counters, any trusted second snapshot (often non-zero on Linux)
        // would spuriously extend waits for a hung child after a transient first-read failure.
        return false;
    }
    if before.cpu_time_total != after.cpu_time_total {
        return true;
    }
    if let (Some(a), Some(b)) = (before.voluntary_ctxt, after.voluntary_ctxt) {
        if a != b {
            return true;
        }
    }
    if let (Some(a), Some(b)) = (before.thread_count, after.thread_count) {
        if a != b {
            return true;
        }
    }
    false
}

const fn silence_outcome_from_pair(
    first: &ChildHealth,
    second: &ChildHealth,
) -> SilenceHealthOutcome {
    if !first.exists {
        return SilenceHealthOutcome::ChildNotRunning;
    }
    if first.zombie {
        return SilenceHealthOutcome::ChildZombie;
    }
    if !second.exists {
        return SilenceHealthOutcome::ChildNotRunning;
    }
    if second.zombie {
        return SilenceHealthOutcome::ChildZombie;
    }
    if health_indicates_progress(first, second) {
        SilenceHealthOutcome::StillBusyExtendWait
    } else {
        SilenceHealthOutcome::AppearsHung
    }
}

pub async fn evaluate_after_acp_silence(pid: u32, grace: Duration) -> SilenceHealthOutcome {
    let first = sample_child_health(pid);
    if !first.exists {
        return SilenceHealthOutcome::ChildNotRunning;
    }
    if first.zombie {
        return SilenceHealthOutcome::ChildZombie;
    }
    tokio::time::sleep(grace).await;
    let second = sample_child_health(pid);
    silence_outcome_from_pair(&first, &second)
}

#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub(crate) mod other;

#[cfg(test)]
#[path = "tests/child_health_tests_root.rs"]
mod child_health_unit_tests;

impl ChildHealth {
    /// `/proc` (or equivalent) has no row for this PID — the child is definitely gone.
    pub(super) fn process_absent() -> Self {
        Self {
            exists: false,
            zombie: false,
            state_hint: None,
            counters_trusted: true,
            cpu_time_total: 0,
            thread_count: None,
            voluntary_ctxt: None,
            sample_time: Instant::now(),
        }
    }

    /// Sampling failed (I/O or parse) or PID is unset — treat as "maybe still running" with no counters.
    ///
    /// Must **not** set [`Self::exists`] to `false`, or [`evaluate_after_acp_silence`] would mis-report
    /// "not running" when we simply could not read OS state.
    pub(super) fn cannot_sample() -> Self {
        Self {
            exists: true,
            zombie: false,
            state_hint: None,
            counters_trusted: false,
            cpu_time_total: 0,
            thread_count: None,
            voluntary_ctxt: None,
            sample_time: Instant::now(),
        }
    }
}

#[cfg(test)]
mod child_health_smoke {
    #[test]
    fn sample_child_health_current_process() {
        let health = crate::child_health::sample_child_health(std::process::id());
        let _ = health.exists;
        let _ = health.zombie;
    }
}

#[cfg(test)]
mod kiss_cov_gate_refs {
    use super::*;
    #[test]
    fn kiss_cov_unit_names() {
        let _ = evaluate_after_acp_silence;
    }
}
