//! macOS sandbox-exec implementation
//!
//! Uses Apple's Seatbelt sandbox (sandbox-exec) for process isolation.
//! The sandbox is defined using Scheme-based Sandbox Profile Language (SBPL).

use super::types::{OsSandboxConfig, OsSandboxMode};
use crate::sandbox::SandboxError;
use std::io::Write;
use tempfile::NamedTempFile;
use tokio::process::Command;

/// Apply macOS sandbox-exec to a command
///
/// This modifies the command to run inside a macOS sandbox by wrapping it
/// with `sandbox-exec -f <profile>`.
pub fn apply_sandbox_exec(
    _cmd: &mut Command,
    config: &OsSandboxConfig,
) -> Result<(), SandboxError> {
    if !config.mode.is_enabled() {
        return Ok(());
    }

    // Generate sandbox profile to validate config, but we cannot apply it
    // to an existing Command due to Tokio Command API limitations.
    // Callers should use `run_sandboxed()` instead for actual sandboxing.
    let profile = generate_sandbox_profile(config)?;
    tracing::debug!("Generated SBPL profile:\n{}", profile);

    Err(SandboxError::InitializationFailed(
        "Cannot apply sandbox-exec to an existing Command. Use run_sandboxed() instead.".into(),
    ))
}

/// Generate a Sandbox Profile Language (SBPL) profile
fn generate_sandbox_profile(config: &OsSandboxConfig) -> Result<String, SandboxError> {
    // Use custom profile if provided
    if let Some(custom) = &config.custom_profile {
        return Ok(custom.clone());
    }

    let mut profile = String::new();

    // Version declaration
    profile.push_str("(version 1)\n");

    // Start with deny-all for strict mode, allow-all for others
    match config.mode {
        OsSandboxMode::Strict => {
            profile.push_str("(deny default)\n");
        }
        _ => {
            profile.push_str("(allow default)\n");
        }
    }

    // Common permissions
    profile.push_str("\n; Common permissions\n");
    profile.push_str("(allow signal)\n");
    profile.push_str("(allow process-fork)\n");
    profile.push_str("(allow sysctl-read)\n");
    profile.push_str("(allow mach-lookup)\n");

    // Allow reading system libraries and frameworks
    profile.push_str("\n; System access\n");
    profile.push_str("(allow file-read*\n");
    profile.push_str("  (subpath \"/usr/lib\")\n");
    profile.push_str("  (subpath \"/usr/share\")\n");
    profile.push_str("  (subpath \"/System\")\n");
    profile.push_str("  (subpath \"/Library/Frameworks\")\n");
    profile.push_str("  (subpath \"/private/var/db\")\n");
    profile.push_str(")\n");

    // Allow process execution if enabled
    if config.allow_process {
        profile.push_str("\n; Process execution\n");
        profile.push_str("(allow process-exec*)\n");
        profile.push_str("(allow file-read*\n");
        profile.push_str("  (subpath \"/usr/bin\")\n");
        profile.push_str("  (subpath \"/bin\")\n");
        profile.push_str("  (subpath \"/usr/sbin\")\n");
        profile.push_str("  (subpath \"/sbin\")\n");
        profile.push_str(")\n");
    }

    // Allow temporary directory if enabled
    if config.allow_tmp {
        profile.push_str("\n; Temporary directory\n");
        profile.push_str("(allow file-read* file-write*\n");
        profile.push_str("  (subpath \"/tmp\")\n");
        profile.push_str("  (subpath \"/private/tmp\")\n");
        profile.push_str("  (subpath \"/var/folders\")\n");
        profile.push_str("  (subpath \"/private/var/folders\")\n");
        profile.push_str(")\n");
    }

    // Working directory access
    if let Some(working_dir) = &config.working_dir {
        let path = working_dir.to_string_lossy();
        profile.push_str("\n; Working directory\n");
        match config.mode {
            OsSandboxMode::ReadOnly => {
                profile.push_str(&format!("(allow file-read* (subpath \"{}\"))\n", path));
            }
            _ => {
                profile.push_str(&format!(
                    "(allow file-read* file-write* (subpath \"{}\"))\n",
                    path
                ));
            }
        }
    }

    // Additional read-only paths
    if !config.read_only_paths.is_empty() {
        profile.push_str("\n; Additional read-only paths\n");
        profile.push_str("(allow file-read*\n");
        for path in &config.read_only_paths {
            profile.push_str(&format!("  (subpath \"{}\")\n", path.to_string_lossy()));
        }
        profile.push_str(")\n");
    }

    // Additional write paths
    if !config.write_paths.is_empty() {
        profile.push_str("\n; Additional write paths\n");
        profile.push_str("(allow file-read* file-write*\n");
        for path in &config.write_paths {
            profile.push_str(&format!("  (subpath \"{}\")\n", path.to_string_lossy()));
        }
        profile.push_str(")\n");
    }

    // Network access
    profile.push_str("\n; Network access\n");
    if config.allow_network {
        profile.push_str("(allow network*)\n");
    } else {
        profile.push_str("(deny network*)\n");
    }

    // Mode-specific restrictions
    match config.mode {
        OsSandboxMode::ReadOnly => {
            profile.push_str("\n; Read-only mode restrictions\n");
            profile.push_str("(deny file-write*)\n");
        }
        OsSandboxMode::NoNetwork => {
            // Network already denied above
        }
        OsSandboxMode::Strict => {
            profile.push_str("\n; Strict mode restrictions\n");
            // Deny most operations by default
        }
        _ => {}
    }

    Ok(profile)
}

/// Write sandbox profile to a temporary file
fn write_profile_to_temp(profile: &str) -> Result<NamedTempFile, SandboxError> {
    let mut file = NamedTempFile::with_suffix(".sb").map_err(|e| {
        SandboxError::InitializationFailed(format!("Failed to create temp file: {}", e))
    })?;

    file.write_all(profile.as_bytes()).map_err(|e| {
        SandboxError::InitializationFailed(format!("Failed to write profile: {}", e))
    })?;

    file.flush().map_err(|e| {
        SandboxError::InitializationFailed(format!("Failed to flush profile: {}", e))
    })?;

    Ok(file)
}

/// Run a command directly with sandbox-exec
///
/// This is a helper function for running a single command in a sandbox.
/// Useful for testing or one-off sandboxed executions.
pub async fn run_sandboxed(
    program: &str,
    args: &[&str],
    config: &OsSandboxConfig,
) -> Result<std::process::Output, SandboxError> {
    let profile = generate_sandbox_profile(config)?;
    let profile_file = write_profile_to_temp(&profile)?;

    let output = tokio::process::Command::new("/usr/bin/sandbox-exec")
        .arg("-f")
        .arg(profile_file.path())
        .arg(program)
        .args(args)
        .output()
        .await
        .map_err(|e| SandboxError::SpawnFailed(e.to_string()))?;

    Ok(output)
}

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

    #[test]
    fn test_generate_profile_read_only() {
        let config = OsSandboxConfig::read_only(PathBuf::from("/tmp/test"));
        let profile = generate_sandbox_profile(&config).unwrap();

        assert!(profile.contains("(version 1)"));
        assert!(profile.contains("(allow default)"));
        assert!(profile.contains("(deny file-write*)"));
    }

    #[test]
    fn test_generate_profile_strict() {
        let config = OsSandboxConfig::strict(PathBuf::from("/tmp/test"));
        let profile = generate_sandbox_profile(&config).unwrap();

        assert!(profile.contains("(version 1)"));
        assert!(profile.contains("(deny default)"));
        assert!(profile.contains("(deny network*)"));
    }

    #[test]
    fn test_generate_profile_no_network() {
        let config = OsSandboxConfig::no_network(PathBuf::from("/tmp/test"));
        let profile = generate_sandbox_profile(&config).unwrap();

        assert!(profile.contains("(deny network*)"));
    }

    #[test]
    fn test_generate_profile_custom() {
        let custom_profile = "(version 1)(allow default)";
        let config =
            OsSandboxConfig::new(OsSandboxMode::Custom).with_custom_profile(custom_profile);

        let profile = generate_sandbox_profile(&config).unwrap();
        assert_eq!(profile, custom_profile);
    }

    #[test]
    fn test_generate_profile_with_paths() {
        let config = OsSandboxConfig::new(OsSandboxMode::ReadOnly)
            .with_working_dir(PathBuf::from("/home/user/project"))
            .with_read_only_path(PathBuf::from("/usr/local"))
            .with_write_path(PathBuf::from("/tmp/output"));

        let profile = generate_sandbox_profile(&config).unwrap();

        assert!(profile.contains("/home/user/project"));
        assert!(profile.contains("/usr/local"));
        assert!(profile.contains("/tmp/output"));
    }
}
