use anyhow::{Context, Result, bail};
use std::{
    fs,
    io::{Read, pipe},
    path::PathBuf,
    process::{Command, Stdio},
};

/// Run a command with a description for a possible error and append the merged stdout and stderr.
/// The boolean in the returned `Result` is true if the command's exit status is success.
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
    let spawn = |mut cmd: Command| {
        // NOTE: The closure drops `cmd` which prevents a pipe deadlock.
        cmd.stdin(Stdio::null())
            .spawn()
            .with_context(|| format!("Failed to run the command `{description}`"))
    };

    let mut handle = if let Some(output) = output {
        let (mut reader, writer) = pipe().with_context(|| {
            format!("Failed to create a pipe to run the command `{description}``")
        })?;

        let writer_clone = writer.try_clone().with_context(|| {
            format!("Failed to clone the pipe writer for the command `{description}`")
        })?;

        cmd.stdout(writer_clone).stderr(writer);
        let handle = spawn(cmd)?;

        reader
            .read_to_end(output)
            .with_context(|| format!("Failed to read the output of the command `{description}`"))?;

        output.push(b'\n');

        handle
    } else {
        cmd.stdout(Stdio::null()).stderr(Stdio::null());
        spawn(cmd)?
    };

    handle
        .wait()
        .with_context(|| format!("Failed to wait on the command `{description}` to exit"))
        .map(|status| status.success())
}

pub struct CmdRunner {
    pub target_dir: PathBuf, // Keep target_dir for binary output
}

impl CmdRunner {
    pub fn build() -> Result<Self> {
        // Check if gcc is available
        if let Err(e) = Command::new("gcc").arg("--version").output() {
            bail!("Failed to find `gcc`. Make sure it's installed and available in PATH: {e}");
        }
        
        // Create target directory for compiled binaries
        let target_dir = PathBuf::from("target");
        fs::create_dir_all(&target_dir)?;
        
        Ok(Self {
            target_dir
        })
    }

    pub fn gcc<'out>(
        &self,
        bin_name: &str,
        output: Option<&'out mut Vec<u8>>,
    ) -> GccCommand<'out> {
        let mut cmd = Command::new("gcc");
        cmd.arg("-Wall").arg("-Wextra").arg("-std=c99").arg("-DTEST_MODE");
        let target_path = self.target_dir.join(bin_name);
        cmd.arg("-o").arg(target_path);

        GccCommand { cmd, output }
    }


    /// The boolean in the returned `Result` is true if the command's exit status is success.
    pub fn run_bin(&self, bin_name: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
        // 7 = "/debug/".len()
        let mut bin_path =
            PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
        bin_path.push(&self.target_dir);
        bin_path.push(bin_name);

        run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)
    }
}

pub struct GccCommand<'out> {
    cmd: Command,
    output: Option<&'out mut Vec<u8>>,
}

impl GccCommand<'_> {
    pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
    where
        I: IntoIterator<Item = &'arg str>,
    {
        self.cmd.args(args);
        self
    }

    pub fn run(self, description: &str) -> anyhow::Result<bool> {
        run_cmd(self.cmd, description, self.output)
    }
}

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

    #[test]
    fn test_run_cmd() {
        let mut cmd = Command::new("echo");
        cmd.arg("Hello");

        let mut output = Vec::with_capacity(8);
        run_cmd(cmd, "echo …", Some(&mut output)).unwrap();

        assert_eq!(output, b"Hello\n\n");
    }
}
