/// Shell execution — each command runs in its own fresh process.
/// On Windows we use powershell.exe -NoProfile so the user's $PROFILE
/// (which might run `clear`) is never loaded.
use std::time::Duration;
use tokio::process::Command;

const DEFAULT_TIMEOUT_MS: u64 = 30_000;

pub struct PersistentShell;

impl PersistentShell {
    pub fn new() -> Self { Self }

    pub async fn run(&mut self, command: &str, timeout_ms: Option<u64>) -> Result<(String, String, i32), String> {
        let timeout_ms = timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS);
        let timeout = Duration::from_millis(timeout_ms);
        #[cfg(windows)]
        {
            let mut cmd = Command::new("powershell.exe");
            cmd.args(["-NoProfile", "-NonInteractive", "-Command", command]);
            run_captured(cmd, timeout).await
        }
        #[cfg(not(windows))]
        {
            let mut cmd = Command::new("sh");
            cmd.args(["-c", command]);
            run_captured(cmd, timeout).await
        }
    }
}

async fn run_captured(
    mut cmd: Command,
    timeout: Duration,
) -> Result<(String, String, i32), String> {
    cmd.stdout(std::process::Stdio::piped())
       .stderr(std::process::Stdio::piped());

    let child = cmd.spawn()
        .map_err(|e| format!("Failed to spawn process: {e}"))?;

    match tokio::time::timeout(timeout, child.wait_with_output()).await {
        Ok(Ok(out)) => Ok((
            String::from_utf8_lossy(&out.stdout).into_owned(),
            String::from_utf8_lossy(&out.stderr).into_owned(),
            out.status.code().unwrap_or(-1),
        )),
        Ok(Err(e)) => Err(format!("Process wait failed: {e}")),
        Err(_)     => Err(format!("Command timed out after {}ms", timeout.as_millis())),
    }
}
