//! AppleScript / JavaScript-for-Automation bridge via `osascript(1)`.
//!
//! The agent-relevant surface is one function: [`run`]. Pass the script
//! source plus a [`Language`], get back stdout/stderr.
//!
//! `osascript` reads from stdin when invoked with `-`, which lets us
//! pass arbitrarily long scripts without quoting nightmares. We also
//! support [`run_with_args`] for scripts that consume positional
//! arguments via AppleScript's `on run argv` handler.
//!
//! # Permissions
//!
//! When an AppleScript controls another application for the first
//! time, macOS surfaces a TCC Automation consent prompt for the
//! *parent process* of `osascript` — i.e. the binary embedding CAR.
//! Preflight via `car-permissions::Permission::Automation` to surface
//! a friendly message before the prompt fires.

use std::process::Stdio;
use tokio::io::AsyncWriteExt;

use crate::{AutomationError, AutomationOutput, Result};

/// Source language understood by `osascript`.
///
/// AppleScript is the historic default; JavaScript-for-Automation
/// (JXA) ships with macOS 10.10+ and is often more pleasant for
/// agents to generate, since LLMs produce cleaner JS than AppleScript.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Language {
    AppleScript,
    JavaScript,
}

impl Language {
    fn osascript_arg(self) -> &'static str {
        match self {
            Language::AppleScript => "AppleScript",
            Language::JavaScript => "JavaScript",
        }
    }
}

/// Run a script, return its stdout/stderr.
///
/// Equivalent to `osascript -l <lang> -` with `script` piped to stdin.
///
/// `timeout` bounds how long the child may run; `None` blocks
/// indefinitely (use sparingly — a misbehaving GUI script can hang).
pub async fn run(
    script: &str,
    lang: Language,
    timeout: Option<std::time::Duration>,
) -> Result<AutomationOutput> {
    run_with_args(script, lang, &[], timeout).await
}

/// Run a script that consumes positional arguments.
///
/// In AppleScript, the script body must define `on run argv` — `argv`
/// is a list of strings. In JXA, define `function run(argv)`. Each
/// `args` element is passed as a separate `osascript` positional.
pub async fn run_with_args(
    script: &str,
    lang: Language,
    args: &[&str],
    timeout: Option<std::time::Duration>,
) -> Result<AutomationOutput> {
    #[cfg(not(target_os = "macos"))]
    {
        let _ = (script, lang, args, timeout);
        return Err(AutomationError::PlatformUnsupported {
            reason: "osascript is only available on macOS",
        });
    }

    #[cfg(target_os = "macos")]
    {
        if script.is_empty() {
            return Err(AutomationError::InvalidInput(
                "script source is empty".into(),
            ));
        }

        let mut cmd = tokio::process::Command::new("/usr/bin/osascript");
        cmd.arg("-l")
            .arg(lang.osascript_arg())
            // `-` reads source from stdin.
            .arg("-")
            .args(args)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());

        let mut child = cmd
            .spawn()
            .map_err(|e| AutomationError::Spawn(format!("osascript: {e}")))?;

        if let Some(mut stdin) = child.stdin.take() {
            stdin.write_all(script.as_bytes()).await?;
            stdin.shutdown().await?;
        }

        let output = match timeout {
            Some(dur) => match tokio::time::timeout(dur, child.wait_with_output()).await {
                Ok(r) => r?,
                Err(_) => {
                    // Timeout fired; the child has been moved into
                    // wait_with_output so we can't kill it from here.
                    // Return a clear error — the process will be
                    // reaped by tokio when its handle is dropped.
                    return Err(AutomationError::SubprocessFailed {
                        exit_code: None,
                        stdout: String::new(),
                        stderr: format!("osascript timed out after {dur:?}"),
                    });
                }
            },
            None => child.wait_with_output().await?,
        };

        let stdout = String::from_utf8_lossy(&output.stdout)
            .trim_end_matches('\n')
            .to_string();
        let stderr = String::from_utf8_lossy(&output.stderr)
            .trim_end_matches('\n')
            .to_string();
        let exit_code = output.status.code();

        if !output.status.success() {
            return Err(AutomationError::SubprocessFailed {
                exit_code,
                stdout,
                stderr,
            });
        }

        Ok(AutomationOutput {
            stdout,
            stderr,
            exit_code,
        })
    }
}

#[cfg(all(test, target_os = "macos"))]
mod tests {
    use super::*;
    use std::time::Duration;

    #[tokio::test]
    async fn runs_trivial_applescript() {
        let out = run(
            "return 1 + 2",
            Language::AppleScript,
            Some(Duration::from_secs(5)),
        )
        .await
        .expect("osascript should succeed");
        assert_eq!(out.stdout, "3");
    }

    #[tokio::test]
    async fn runs_trivial_jxa() {
        let out = run(
            "1 + 2",
            Language::JavaScript,
            Some(Duration::from_secs(5)),
        )
        .await
        .expect("osascript should succeed");
        assert_eq!(out.stdout, "3");
    }

    #[tokio::test]
    async fn passes_positional_args() {
        let script = r#"
            on run argv
                return (item 1 of argv) & "-" & (item 2 of argv)
            end run
        "#;
        let out = run_with_args(
            script,
            Language::AppleScript,
            &["hello", "world"],
            Some(Duration::from_secs(5)),
        )
        .await
        .expect("osascript should succeed");
        assert_eq!(out.stdout, "hello-world");
    }

    #[tokio::test]
    async fn surfaces_script_error() {
        let err = run(
            "error \"boom\" number 42",
            Language::AppleScript,
            Some(Duration::from_secs(5)),
        )
        .await
        .expect_err("script should fail");
        match err {
            AutomationError::SubprocessFailed {
                exit_code, stderr, ..
            } => {
                assert_eq!(exit_code, Some(1));
                assert!(stderr.contains("boom"), "stderr was: {stderr}");
            }
            other => panic!("expected SubprocessFailed, got {other:?}"),
        }
    }

    #[tokio::test]
    async fn empty_script_rejected() {
        let err = run("", Language::AppleScript, None).await.unwrap_err();
        assert!(matches!(err, AutomationError::InvalidInput(_)));
    }
}

#[cfg(all(test, not(target_os = "macos")))]
mod tests_other {
    use super::*;

    #[tokio::test]
    async fn returns_unsupported_off_macos() {
        let err = run("return 1", Language::AppleScript, None).await.unwrap_err();
        assert!(matches!(err, AutomationError::PlatformUnsupported { .. }));
    }
}
