#[cfg(unix)]
use std::io::Read;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(unix)]
use std::path::Path;
#[cfg(unix)]
use std::process::{Child, Command, Stdio};
#[cfg(unix)]
use std::thread::{self, JoinHandle};
#[cfg(unix)]
use std::time::{Duration, Instant};

#[cfg(unix)]
pub type PipedChildHandles = (Child, JoinHandle<Vec<u8>>, JoinHandle<Vec<u8>>);

#[cfg(unix)]
fn kill_bin() -> &'static Path {
    if Path::new("/bin/kill").is_file() {
        Path::new("/bin/kill")
    } else {
        Path::new("/usr/bin/kill")
    }
}

#[cfg(unix)]
fn kill_process_group(pid: u32) {
    let _ = Command::new(kill_bin())
        .args(["-KILL", &format!("-{pid}")])
        .status();
}

#[cfg(unix)]
fn join_stdio_reader(jh: JoinHandle<Vec<u8>>) -> std::io::Result<Vec<u8>> {
    jh.join()
        .map_err(|_| std::io::Error::other("malvin subprocess reader panicked"))
}

#[cfg(unix)]
fn output_joined(
    status: std::process::ExitStatus,
    stdout_jh: JoinHandle<Vec<u8>>,
    stderr_jh: JoinHandle<Vec<u8>>,
) -> std::io::Result<std::process::Output> {
    Ok(std::process::Output {
        status,
        stdout: join_stdio_reader(stdout_jh)?,
        stderr: join_stdio_reader(stderr_jh)?,
    })
}

#[cfg(unix)]
pub fn spawn_piped_process_group(cmd: &mut Command) -> std::io::Result<PipedChildHandles> {
    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
    cmd.process_group(0);
    let mut child = cmd.spawn()?;
    let stdout = child.stdout.take().expect("stdout piped");
    let stderr = child.stderr.take().expect("stderr piped");
    let stdout_jh = thread::spawn(move || {
        let mut stdout = stdout;
        let mut v = Vec::new();
        let _ = stdout.read_to_end(&mut v);
        v
    });
    let stderr_jh = thread::spawn(move || {
        let mut stderr = stderr;
        let mut v = Vec::new();
        let _ = stderr.read_to_end(&mut v);
        v
    });
    Ok((child, stdout_jh, stderr_jh))
}

#[cfg(unix)]
pub fn wait_child_with_timeout(
    mut child: Child,
    stdout_jh: JoinHandle<Vec<u8>>,
    stderr_jh: JoinHandle<Vec<u8>>,
    deadline: Instant,
) -> std::io::Result<std::process::Output> {
    loop {
        match child.try_wait() {
            Ok(Some(status)) => return output_joined(status, stdout_jh, stderr_jh),
            Ok(None) => {
                if Instant::now() > deadline {
                    kill_process_group(child.id());
                    let _ = child.wait();
                    let stdout = join_stdio_reader(stdout_jh)?;
                    let stderr = join_stdio_reader(stderr_jh)?;
                    let stdout_text = String::from_utf8_lossy(&stdout);
                    let stderr_text = String::from_utf8_lossy(&stderr);
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::TimedOut,
                        format!(
                            "malvin subprocess timed out; stdout={stdout_text:?}; stderr={stderr_text:?}"
                        ),
                    ));
                }
                std::thread::sleep(Duration::from_millis(20));
            }
            Err(e) => {
                let _ = stdout_jh.join();
                let _ = stderr_jh.join();
                return Err(e);
            }
        }
    }
}
