//! #506 — emit a kernel-enforced sandbox profile alongside the
//! compiled binary.
//!
//! Today: macOS only — emits `<binary>.sandbox`, a sandbox-exec
//! profile derived from the build's reachable stdlib surface. The
//! host invokes the binary via:
//!
//! ```bash
//! sandbox-exec -f myapp.sandbox myapp
//! ```
//!
//! Future:
//!
//! - **Linux**: `seccomp` BPF filter (binary blob) + landlock for FS
//!   scoping. Tracked as a #506 follow-up.
//! - **Windows**: AppContainer manifest. Less universal across
//!   server / desktop installs; tracked as a #506 follow-up.
//! - **HarmonyOS / Android / web / etc.**: kernel sandbox not
//!   applicable (or already-applied by the platform sandbox at
//!   process launch).
//!
//! The MVP derives the profile from a handful of high-signal flags
//! on `CompilationContext`:
//!
//! - `needs_js_runtime` — links QuickJS, which JIT-allocates
//!   executable memory and needs `mmap` write+exec carve-outs.
//! - `native_module_imports` — the set of imported stdlib modules
//!   (used to drive allow/deny rules).
//! - `uses_fetch` / per-module `fetch` use — toggles outbound network.
//!
//! Reviewers / users can customise the emitted profile manually; the
//! goal of the MVP is to give every build a *starting point* instead
//! of an empty file.

use crate::commands::compile::CompilationContext;
use std::io::Write;
use std::path::Path;

/// Emit a sandbox-exec profile for the given binary path. Best-effort:
/// errors are surfaced to the caller (the compile driver), which
/// logs and continues — the profile is observational, not a
/// correctness gate.
pub fn emit_macos_sandbox_profile(
    ctx: &CompilationContext,
    binary_path: &Path,
) -> std::io::Result<std::path::PathBuf> {
    let out = binary_path.with_extension("sandbox");
    let body = build_macos_profile(ctx);
    let mut f = std::fs::File::create(&out)?;
    f.write_all(body.as_bytes())?;
    Ok(out)
}

/// Build the textual sandbox-exec profile for `ctx`. Returned
/// separately from the file write so tests can pin the contents
/// without touching the filesystem.
pub fn build_macos_profile(ctx: &CompilationContext) -> String {
    let needs_jit = ctx.needs_js_runtime;
    let needs_child_process = imports_module(ctx, "child_process");
    let needs_net = imports_module(ctx, "http")
        || imports_module(ctx, "https")
        || imports_module(ctx, "net")
        || imports_module(ctx, "tls")
        || imports_module(ctx, "dns")
        || imports_module(ctx, "ws")
        || imports_module(ctx, "axios")
        || imports_module(ctx, "node-fetch")
        || imports_module(ctx, "redis")
        || imports_module(ctx, "ioredis")
        || ctx.uses_fetch;
    let needs_fs_write = imports_module(ctx, "fs")
        // The HIR-level FsWriteFileSync / FsMkdirSync / FsAppendFileSync
        // variants don't surface in `native_module_imports`, but if `fs`
        // is reachable at all we can't distinguish read-only vs
        // read-write cheaply from here. A tighter derivation that
        // walks the HIR is the natural follow-up (would also exclude
        // packages that only do `fs.readFileSync`).
        ;

    let mut out = String::new();
    out.push_str(";; perry sandbox profile (auto-generated by `perry compile --emit-sandbox`)\n");
    out.push_str(";;\n");
    out.push_str(";; Apply with:\n");
    out.push_str(";;   sandbox-exec -f <binary>.sandbox <binary>\n");
    out.push_str(";;\n");
    out.push_str(";; Tighten or relax this manually — Perry's MVP derives a\n");
    out.push_str(";; conservative starting point from a handful of build-time signals\n");
    out.push_str(";; (jsruntime linked?  child_process imported?  fs/net imported?).\n");
    out.push_str(";; Issue #506 tracks the per-API HIR-driven refinement.\n\n");

    out.push_str("(version 1)\n");
    out.push_str("(deny default)\n\n");

    // Process must be able to read its own resources + system libs.
    out.push_str(";; --- own process resources ---\n");
    out.push_str("(allow process-info-pidinfo (target self))\n");
    out.push_str("(allow process-info-pidfdinfo (target self))\n");
    out.push_str("(allow signal (target self))\n");
    out.push_str("(allow sysctl-read)\n");
    out.push_str("(allow mach-lookup)\n");
    out.push_str("(allow iokit-open)\n\n");

    // Always allow read on system locations.
    out.push_str(";; --- file read (system libs + own binary) ---\n");
    out.push_str("(allow file-read*\n");
    out.push_str("    (subpath \"/usr/lib\")\n");
    out.push_str("    (subpath \"/usr/share\")\n");
    out.push_str("    (subpath \"/System\")\n");
    out.push_str("    (subpath \"/Library\")\n");
    out.push_str("    (subpath \"/private/var/folders\")\n");
    out.push_str("    (subpath \"/private/tmp\")\n");
    out.push_str("    (subpath \"/tmp\")\n");
    out.push_str("    (literal \"/dev/null\")\n");
    out.push_str("    (literal \"/dev/urandom\")\n");
    out.push_str("    (literal \"/dev/random\"))\n\n");

    if needs_fs_write {
        out.push_str(";; --- file write (build imports `fs`) ---\n");
        out.push_str("(allow file-write*\n");
        out.push_str("    (subpath \"/tmp\")\n");
        out.push_str("    (subpath \"/private/tmp\")\n");
        out.push_str("    (subpath \"/private/var/folders\"))\n\n");
    } else {
        out.push_str(";; --- file write denied (no `fs` import detected) ---\n");
        out.push_str("(deny file-write*)\n\n");
    }

    if needs_net {
        out.push_str(";; --- network (build imports http/https/net/tls/fetch/...) ---\n");
        out.push_str("(allow network*)\n\n");
    } else {
        out.push_str(";; --- network denied (no http/https/net/tls/fetch import detected) ---\n");
        out.push_str("(deny network*)\n\n");
    }

    if needs_child_process {
        out.push_str(";; --- process spawn (build imports `child_process`) ---\n");
        out.push_str("(allow process-fork)\n");
        out.push_str("(allow process-exec)\n\n");
    } else {
        out.push_str(";; --- process spawn denied (no `child_process` import detected) ---\n");
        out.push_str("(deny process-fork)\n");
        out.push_str("(deny process-exec)\n\n");
    }

    if needs_jit {
        out.push_str(";; --- JIT (perry-jsruntime linked) ---\n");
        out.push_str(";; QuickJS allocates write+exec pages for compiled bytecode.\n");
        out.push_str("(allow process-info-codesigning-status)\n");
        out.push_str("(allow dynamic-code-generation)\n\n");
    }

    out
}

fn imports_module(ctx: &CompilationContext, name: &str) -> bool {
    // The compile context tracks user-imported module specifiers in
    // `native_module_imports` (a `BTreeSet<String>`).
    ctx.native_module_imports.contains(name)
}

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

    fn empty_ctx() -> CompilationContext {
        CompilationContext::new(PathBuf::from("/tmp"))
    }

    #[test]
    fn minimal_program_denies_everything() {
        // No imports, no fetch — every privileged surface should be
        // denied so the user can review and selectively relax.
        let ctx = empty_ctx();
        let profile = build_macos_profile(&ctx);
        assert!(profile.contains("(deny default)"));
        assert!(
            profile.contains("(deny network*)"),
            "network must deny:\n{profile}"
        );
        assert!(
            profile.contains("(deny file-write*)"),
            "fs-write must deny:\n{profile}"
        );
        assert!(
            profile.contains("(deny process-fork)"),
            "process-fork must deny:\n{profile}"
        );
    }

    #[test]
    fn fetch_unlocks_network() {
        let mut ctx = empty_ctx();
        ctx.uses_fetch = true;
        let profile = build_macos_profile(&ctx);
        assert!(
            profile.contains("(allow network*)"),
            "fetch must allow net:\n{profile}"
        );
    }

    #[test]
    fn http_import_unlocks_network() {
        let mut ctx = empty_ctx();
        ctx.native_module_imports.insert("http".into());
        let profile = build_macos_profile(&ctx);
        assert!(profile.contains("(allow network*)"));
    }

    #[test]
    fn child_process_import_unlocks_spawn() {
        let mut ctx = empty_ctx();
        ctx.native_module_imports.insert("child_process".into());
        let profile = build_macos_profile(&ctx);
        assert!(profile.contains("(allow process-fork)"));
        assert!(profile.contains("(allow process-exec)"));
    }

    #[test]
    fn fs_import_unlocks_write() {
        let mut ctx = empty_ctx();
        ctx.native_module_imports.insert("fs".into());
        let profile = build_macos_profile(&ctx);
        assert!(profile.contains("(allow file-write*"));
        assert!(!profile.contains("(deny file-write*)"));
    }

    #[test]
    fn jsruntime_unlocks_dynamic_code() {
        let mut ctx = empty_ctx();
        ctx.needs_js_runtime = true;
        let profile = build_macos_profile(&ctx);
        assert!(
            profile.contains("(allow dynamic-code-generation)"),
            "jsruntime must allow W+X mmap:\n{profile}"
        );
    }

    #[test]
    fn always_allows_reading_system_libs() {
        // Even the most-paranoid build needs to dlopen system libs;
        // otherwise the process can't even reach main().
        let ctx = empty_ctx();
        let profile = build_macos_profile(&ctx);
        assert!(profile.contains("(allow file-read*"));
        assert!(profile.contains("(subpath \"/usr/lib\")"));
        assert!(profile.contains("(subpath \"/System\")"));
    }

    #[test]
    fn header_documents_usage() {
        let ctx = empty_ctx();
        let profile = build_macos_profile(&ctx);
        assert!(
            profile.contains("sandbox-exec -f"),
            "must document usage:\n{profile}"
        );
        assert!(profile.contains("#506"), "must cite tracker:\n{profile}");
    }
}
