use crate::utils::Utils;
use anyhow::{Context, Error as AnyhowError};
use derive_more::{Constructor, From, Into};
use std::{
    borrow::Borrow,
    ffi::OsStr,
    io::Error as IoError,
    path::Path,
    process::{ExitStatus, Stdio},
};
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};

#[derive(From, Into)]
pub struct ProcessBuilder {
    command: Command,
}

impl ProcessBuilder {
    const STDIO_ERROR_MESSAGE: &str = "unable to set up stdio for process";

    pub fn new(cmd: impl AsRef<OsStr>) -> Self {
        let mut command = Command::new(cmd);

        command
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());

        command.into()
    }

    pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
        self.command.arg(arg);

        self
    }

    pub fn args(&mut self, args: impl IntoIterator<Item: AsRef<OsStr>>) -> &mut Self {
        self.command.args(args);

        self
    }

    pub fn env(&mut self, env: impl IntoIterator<Item: Borrow<(impl AsRef<OsStr>, impl AsRef<OsStr>)>>) -> &mut Self {
        for env_entry in env {
            let (env_var_name, env_var_val) = env_entry.borrow();

            self.command.env(env_var_name, env_var_val);
        }

        self
    }

    pub fn current_dirpath<P: AsRef<Path>>(&mut self, current_dirpath: impl Into<Option<P>>) -> &mut Self {
        if let Some(current_dirpath) = current_dirpath.into() {
            self.command.current_dir(current_dirpath);
        }

        self
    }

    fn take_stdio<T>(stdio: &mut Option<T>) -> Result<T, AnyhowError> {
        stdio.take().context(Self::STDIO_ERROR_MESSAGE)
    }

    pub const fn command_mut(&mut self) -> &mut Command {
        &mut self.command
    }

    pub fn build(&mut self) -> Result<Process, AnyhowError> {
        let mut child = self.command.spawn()?;
        let stdin = Self::take_stdio(&mut child.stdin)?;
        let stdout = Self::take_stdio(&mut child.stdout)?;
        let stderr = Self::take_stdio(&mut child.stderr)?;
        let process = Process::new(child, stdin, stdout, stderr);

        process.ok()
    }
}

#[derive(Constructor)]
pub struct Process {
    child: Child,
    stdin: ChildStdin,
    stdout: ChildStdout,
    stderr: ChildStderr,
}

impl Process {
    pub const fn stdin_mut(&mut self) -> &mut ChildStdin {
        &mut self.stdin
    }

    pub const fn stdout_mut(&mut self) -> &mut ChildStdout {
        &mut self.stdout
    }

    pub const fn stderr_mut(&mut self) -> &mut ChildStderr {
        &mut self.stderr
    }

    #[must_use]
    pub fn into_parts(self) -> (Child, ChildStdin, ChildStdout, ChildStderr) {
        (self.child, self.stdin, self.stdout, self.stderr)
    }

    pub async fn run(mut self) -> Result<ExitStatus, IoError> {
        self.stdin.mem_drop();
        self.stdout.mem_drop();
        self.stderr.mem_drop();
        self.child.wait().await?.ok()
    }
}
