/*
Module: sandboxing

Build platform wrappers and produce ExecEnv for execution. Owns low‑level
sandbox placement and transformation of portable CommandSpec into a
ready‑to‑spawn environment.
*/

use crate::product::agent::exec::ExecExpiration;
use crate::product::agent::exec::ExecToolCallOutput;
use crate::product::agent::exec::SandboxType;
use crate::product::agent::exec::StdoutStream;
use crate::product::agent::exec::execute_exec_env;
use crate::product::agent::landlock::create_linux_sandbox_command_args;
use crate::product::agent::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::product::agent::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
#[cfg(target_os = "macos")]
use crate::product::agent::seatbelt::create_seatbelt_command_args;
#[cfg(target_os = "macos")]
use crate::product::agent::spawn::LHA_SANDBOX_ENV_VAR;
use crate::product::agent::spawn::LHA_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use crate::product::agent::tools::sandboxing::SandboxablePreference;
use crate::product::protocol::config_types::WindowsSandboxLevel;
pub use crate::product::protocol::models::SandboxPermissions;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;

#[derive(Debug)]
pub struct CommandSpec {
    pub program: String,
    pub args: Vec<String>,
    pub cwd: PathBuf,
    pub env: HashMap<String, String>,
    pub expiration: ExecExpiration,
    pub sandbox_permissions: SandboxPermissions,
    pub justification: Option<String>,
}

#[derive(Debug)]
pub struct ExecEnv {
    pub command: Vec<String>,
    pub cwd: PathBuf,
    pub env: HashMap<String, String>,
    pub expiration: ExecExpiration,
    pub sandbox: SandboxType,
    pub windows_sandbox_level: WindowsSandboxLevel,
    pub sandbox_permissions: SandboxPermissions,
    pub justification: Option<String>,
    pub arg0: Option<String>,
}

pub enum SandboxPreference {
    Auto,
    Require,
    Forbid,
}

#[derive(Debug, thiserror::Error)]
pub(crate) enum SandboxTransformError {
    #[error("missing lha-linux-sandbox executable path")]
    MissingLinuxSandboxExecutable,
    #[cfg(not(target_os = "macos"))]
    #[error("seatbelt sandbox is only available on macOS")]
    SeatbeltUnavailable,
}

#[derive(Default)]
pub struct SandboxManager;

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

    pub(crate) fn select_initial(
        &self,
        policy: &SandboxPolicy,
        pref: SandboxablePreference,
        windows_sandbox_level: WindowsSandboxLevel,
    ) -> SandboxType {
        match pref {
            SandboxablePreference::Forbid => SandboxType::None,
            SandboxablePreference::Require => {
                // Require a platform sandbox when available; on Windows this
                // respects the experimental_windows_sandbox feature.
                crate::product::agent::safety::get_platform_sandbox(
                    windows_sandbox_level != WindowsSandboxLevel::Disabled,
                )
                .unwrap_or(SandboxType::None)
            }
            SandboxablePreference::Auto => match policy {
                SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
                    SandboxType::None
                }
                _ => crate::product::agent::safety::get_platform_sandbox(
                    windows_sandbox_level != WindowsSandboxLevel::Disabled,
                )
                .unwrap_or(SandboxType::None),
            },
        }
    }

    pub(crate) fn transform(
        &self,
        mut spec: CommandSpec,
        policy: &SandboxPolicy,
        sandbox: SandboxType,
        sandbox_policy_cwd: &Path,
        codex_linux_sandbox_exe: Option<&PathBuf>,
        windows_sandbox_level: WindowsSandboxLevel,
    ) -> Result<ExecEnv, SandboxTransformError> {
        let mut env = spec.env;
        if !policy.has_full_network_access() {
            env.insert(
                LHA_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(),
                "1".to_string(),
            );
        }

        let mut command = Vec::with_capacity(1 + spec.args.len());
        command.push(spec.program);
        command.append(&mut spec.args);

        let (command, sandbox_env, arg0_override) = match sandbox {
            SandboxType::None => (command, HashMap::new(), None),
            #[cfg(target_os = "macos")]
            SandboxType::MacosSeatbelt => {
                let mut seatbelt_env = HashMap::new();
                seatbelt_env.insert(LHA_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
                let mut args =
                    create_seatbelt_command_args(command.clone(), policy, sandbox_policy_cwd);
                let mut full_command = Vec::with_capacity(1 + args.len());
                full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
                full_command.append(&mut args);
                (full_command, seatbelt_env, None)
            }
            #[cfg(not(target_os = "macos"))]
            SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
            SandboxType::LinuxSeccomp => {
                let exe = codex_linux_sandbox_exe
                    .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
                let mut args =
                    create_linux_sandbox_command_args(command.clone(), policy, sandbox_policy_cwd);
                let mut full_command = Vec::with_capacity(1 + args.len());
                full_command.push(exe.to_string_lossy().to_string());
                full_command.append(&mut args);
                (
                    full_command,
                    HashMap::new(),
                    Some("lha-linux-sandbox".to_string()),
                )
            }
            // On Windows, the restricted token sandbox executes in-process via the
            // lha-windows-sandbox crate. We leave the command unchanged here and
            // branch during execution based on the sandbox type.
            #[cfg(target_os = "windows")]
            SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None),
            // When building for non-Windows targets, this variant is never constructed.
            #[cfg(not(target_os = "windows"))]
            SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None),
        };

        env.extend(sandbox_env);

        Ok(ExecEnv {
            command,
            cwd: spec.cwd,
            env,
            expiration: spec.expiration,
            sandbox,
            windows_sandbox_level,
            sandbox_permissions: spec.sandbox_permissions,
            justification: spec.justification,
            arg0: arg0_override,
        })
    }

    pub fn denied(&self, sandbox: SandboxType, out: &ExecToolCallOutput) -> bool {
        crate::product::agent::exec::is_likely_sandbox_denied(sandbox, out)
    }
}

pub async fn execute_env(
    env: ExecEnv,
    policy: &SandboxPolicy,
    stdout_stream: Option<StdoutStream>,
) -> crate::product::agent::error::Result<ExecToolCallOutput> {
    execute_exec_env(env, policy, stdout_stream).await
}
