use std::fs::File;
use std::path::{Path, PathBuf};

pub const MAX_STRING_LENGTH: usize = 8 * 1024;
pub const MAX_COMMAND_ARGS: usize = 128;
pub const MAX_SECRET_NAME_LENGTH: usize = 256;
const BLOCKED_SHELL_META: &[char] = &[
    ';', '|', '&', '>', '<', '`', '$', '(', ')', '{', '}', '[', ']', '*', '?', '!', '\'', '"',
];

const BLOCKED_INTERPRETER_NAMES: &[&str] = &[
    "sh",
    "bash",
    "dash",
    "zsh",
    "fish",
    "ksh",
    "csh",
    "tcsh",
    "powershell",
    "pwsh",
    "cmd",
    "python",
    "python3",
    "python2",
    "node",
    "node.js",
    "ruby",
    "perl",
    "php",
];

pub fn require_trimmed_non_empty(value: &str, field: &str) -> Result<(), String> {
    if value.trim().is_empty() {
        return Err(format!("{field} must not be empty"));
    }
    Ok(())
}

pub fn validate_bounded_string(value: &str, field: &str, max_len: usize) -> Result<(), String> {
    if value.len() > max_len {
        return Err(format!("{field} exceeds maximum length of {max_len} bytes"));
    }
    Ok(())
}

pub fn validate_cli_safe_string(value: &str, field: &str) -> Result<(), String> {
    if value.contains('\0') || value.contains('\n') || value.contains('\r') {
        return Err(format!("{field} contains forbidden control characters"));
    }
    if value.chars().any(|c| BLOCKED_SHELL_META.contains(&c)) {
        return Err(format!(
            "{field} contains shell metacharacters that are not permitted"
        ));
    }
    Ok(())
}

pub fn validate_secret_name(value: &str, field: &str) -> Result<(), String> {
    require_trimmed_non_empty(value, field)?;
    validate_bounded_string(value, field, MAX_SECRET_NAME_LENGTH)?;
    // Reject path-traversal characters BEFORE we open the vault.
    // Without this, an MCP client could probe for arbitrary secret
    // names that ultimately fail at the vault layer — but only AFTER
    // the platform passphrase dialog has already popped on the user's
    // screen. That wastes user attention and leaks a timing oracle
    // (popup appearance latency reveals whether the vault would
    // unlock for that path). Bounce these here so the dialog never
    // fires for malformed input.
    if value.contains('/') || value.contains('\\') || value.contains("..") {
        return Err(format!(
            "{field} must not contain path separators or '..' \
             (use a flat secret identifier, e.g. 'openai-api')"
        ));
    }
    if value.contains('\0') || value.contains('\n') || value.contains('\r') {
        return Err(format!("{field} must not contain null bytes or newlines"));
    }
    Ok(())
}

pub fn validate_command(command: &[String]) -> Result<(), String> {
    if command.is_empty() {
        return Err("command must not be empty".to_string());
    }
    if command.len() > MAX_COMMAND_ARGS {
        return Err(format!(
            "command has too many arguments (max {MAX_COMMAND_ARGS})"
        ));
    }
    if command[0].trim().is_empty() {
        return Err("command executable must not be empty".to_string());
    }
    validate_bounded_string(&command[0], "command executable", MAX_STRING_LENGTH)?;
    validate_cli_safe_string(&command[0], "command executable")?;
    let exe_base = Path::new(command[0].trim())
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("")
        .to_ascii_lowercase();
    if BLOCKED_INTERPRETER_NAMES.contains(&exe_base.as_str()) {
        return Err(
            "command executable must not be a shell interpreter (use a real binary)".to_string(),
        );
    }
    if command.len() >= 2 {
        let a1 = command[1].as_str();
        if a1 == "-c" || a1.eq_ignore_ascii_case("/c") {
            return Err("refusing embedded shell commands (argv contains -c or /c)".to_string());
        }
    }
    for part in command.iter().skip(1) {
        if part.trim().is_empty() {
            return Err("command arguments must not be empty or whitespace-only".to_string());
        }
        validate_bounded_string(part, "command argument", MAX_STRING_LENGTH)?;
        validate_cli_safe_string(part, "command argument")?;
    }
    Ok(())
}

pub fn parse_sandbox_profile(raw: Option<&str>) -> Result<envseal::sandbox::SandboxTier, String> {
    envseal::ops::parse_sandbox_profile(raw).map_err(|e| e.to_string())
}

/// Validate that `path` (from untrusted MCP input) refers to a location that
/// does not escape the current working directory.  Returns the resolved
/// absolute path on success.
///
/// This guards against `../../etc/passwd`-style path traversal.  Absolute
/// paths are accepted only when they remain within CWD after normalization.
pub fn validate_path_within_cwd(path: &str, field: &str) -> Result<PathBuf, String> {
    envseal::ops::validate_path_within_cwd(path, field, MAX_STRING_LENGTH)
}

pub fn open_readonly_nofollow(path: &Path, field: &str) -> Result<File, String> {
    let mut opts = std::fs::OpenOptions::new();
    opts.read(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        opts.custom_flags(libc::O_NOFOLLOW);
    }
    opts.open(path).map_err(|e| match e.kind() {
        std::io::ErrorKind::NotFound => format!("{field} not found"),
        std::io::ErrorKind::PermissionDenied => format!("permission denied reading {field}"),
        _ => format!("failed to open {field}: {e}"),
    })
}
