use std::{path::PathBuf, process::Stdio};
use tokio::{
    io::{AsyncBufReadExt, BufReader},
    process::Command as Cmd,
};

#[derive(Debug)]
pub struct Command {
    command: Cmd,
}

impl Command {
    pub fn new(cmd: impl Into<String>) -> Self {
        if cfg!(target_os = "windows") {
            Command::powershell(cmd)
        } else {
            Command::bash(cmd)
        }
    }

    pub fn powershell(cmd: impl Into<String>) -> Self {
        let cmd: String = cmd.into();
        let mut command = Cmd::new("powershell.exe");

        command
            .arg("-NoProfile")
            .arg("-NonInteractive")
            .arg("-Command")
            .arg(cmd);

        Command { command }
    }

    pub fn bash(cmd: impl Into<String>) -> Self {
        let cmd: String = cmd.into();
        let mut command = Cmd::new("bash");

        command.arg("-c").arg(cmd);

        Command { command }
    }

    #[allow(unused)]
    pub fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
        self.command.env(key.into(), value.into());

        self
    }

    #[allow(unused)]
    pub fn dir(&mut self, dir: &PathBuf) -> &mut Self {
        self.command.current_dir(dir);

        self
    }

    #[allow(unused)]
    pub fn arg<S>(&mut self, arg: S) -> &mut Self
    where
        S: AsRef<std::ffi::OsStr>,
    {
        self.command.arg(arg);

        self
    }

    pub async fn exec(&mut self) -> anyhow::Result<String> {
        let output = self.command.output().await?;

        if output.status.success() {
            let stdout = String::from_utf8(output.stdout)?;
            return Ok(stdout.trim().to_string());
        }

        let stderr = String::from_utf8(output.stderr)?;
        Err(anyhow::anyhow!(stderr))
    }

    pub async fn run(&mut self) -> anyhow::Result<()> {
        let mut child = self
            .command
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        let out = child
            .stdout
            .take()
            .ok_or(anyhow::anyhow!("Failed to get stdout from child process"))?;
        let err = child
            .stderr
            .take()
            .ok_or(anyhow::anyhow!("Failed to get stderr from child process"))?;
        let out = BufReader::new(out);
        let err = BufReader::new(err);

        let mut lines = out.lines();
        let mut errors = err.lines();

        loop {
            tokio::select! {
                line = lines.next_line() => {
                    match line {
                        Ok(Some(line)) => {
                            println!("{}", line);
                        }
                        Ok(None) => {
                            break;
                        }
                        Err(err) => {
                            println!("Error {}", err.to_string());
                            break;
                        }
                    }
                }
                error = errors.next_line() => {
                    match error {
                        Ok(Some(error)) => {
                            println!("{}", error);
                        }
                        Ok(None) => {
                            break;
                        }
                        Err(err) => {
                            println!("1Error {}", err.to_string());
                            break;
                        }
                    }
                }
            }
        }

        let status = child.wait().await?;

        if !status.success() {
            return Err(anyhow::anyhow!("Command failed"));
        }

        Ok(())
    }
}
