#import "std.sx";

// =====================================================================
// process.sx — subprocess + environment stdlib (POSIX backend).
//
// Scope (Phase 1A): one entry point `run(cmd)` that shells out to
// /bin/sh, captures stdout, returns exit code + stdout. Plus
// `env(name)` / `find_executable(name)`. The bundler uses these to
// invoke `codesign`, `plutil`, `security`, `aapt2`, `javac`, `d8`,
// `keytool`, `apksigner`.
//
// Roadmap: phase 1B replaces `popen` with `posix_spawn` + pipes so
// we can capture stderr separately and pass argv without shell
// quoting. Until then, callers responsible for quoting + use 2>&1
// to fold stderr into the captured stream.
// =====================================================================

libc :: #library "c";

// ── Low-level libc bindings ─────────────────────────────────────────

popen  :: (cmd: [:0]u8, mode: [:0]u8) -> *void #foreign libc;
pclose :: (stream: *void) -> s32 #foreign libc;
fread  :: (ptr: [*]u8, size: usize, nmemb: usize, stream: *void) -> usize #foreign libc;
feof   :: (stream: *void) -> s32 #foreign libc;
getenv :: (name: [:0]u8) -> *u8 #foreign libc;
strlen :: (s: *u8) -> usize #foreign libc;
system :: (cmd: [:0]u8) -> s32 #foreign libc;

// ── Public types ─────────────────────────────────────────────────────

ProcessResult :: struct {
    /// Exit code as reported by `WEXITSTATUS(status)`. 0 = success.
    /// Note: doesn't distinguish "killed by signal" from "exited
    /// non-zero"; phase 1B will return a tagged union.
    exit_code: s32;
    stdout:    string;
}

// ── Public API ───────────────────────────────────────────────────────

// Run a shell command, capture stdout, wait for exit. Returns null if
// the shell itself couldn't be spawned. A non-zero exit_code with
// valid stdout means the command ran and exited non-zero — distinct
// from spawn failure.
//
// `cmd` is interpreted by /bin/sh — callers MUST quote arguments
// containing spaces or shell metacharacters. To capture stderr along
// with stdout, append " 2>&1" to the command.
run :: (cmd: [:0]u8) -> ?ProcessResult {
    f := popen(cmd, "r");
    if cast(s64) f == 0 { return null; }

    out := "";
    buf := cstring(4096);
    loop := true;
    while loop {
        n := fread(buf.ptr, 1, 4096, f);
        if n == 0 { loop = false; }
        if n > 0 {
            chunk : string = ---;
            chunk.ptr = buf.ptr;
            chunk.len = cast(s64) n;
            out = concat(out, chunk);
        }
    }
    raw_status := pclose(f);
    if raw_status < 0 { return null; }
    // POSIX wait(2) status encoding: low byte = signal (if signaled),
    // next byte = exit code (if normally exited). For our MVP we just
    // surface the exit-code byte; the signal case is folded into the
    // non-zero return.
    exit_code := (raw_status >> 8) & 0xFF;
    if exit_code == 0 {
        if (raw_status & 0x7F) != 0 {
            // Killed by signal — surface as a non-zero exit.
            exit_code = 128 + (raw_status & 0x7F);
        }
    }
    ProcessResult.{ exit_code = exit_code, stdout = out };
}

// Read an environment variable. Returns null if unset; an empty
// string if set to "".
env :: (name: [:0]u8) -> ?string {
    p := getenv(name);
    addr : s64 = xx p;
    if addr == 0 { return null; }
    n := strlen(p);
    if n == 0 { return ""; }
    buf := cstring(cast(s64) n);
    memcpy(buf.ptr, xx p, cast(s64) n);
    buf;
}

// Locate an executable by walking `$PATH`. Returns the absolute path
// to the first hit, or null if not found anywhere. Uses `command -v`
// under the shell; cheap and matches what the bundler ultimately
// shells out to anyway.
find_executable :: (name: [:0]u8) -> ?string {
    // Compose `command -v <name>` — name is assumed shell-safe
    // (executable names like `codesign`, `javac`, `aapt2`).
    cmd := concat("command -v ", name);
    // Need null-terminated for popen.
    cmd_z := cstring(cmd.len);
    memcpy(cmd_z.ptr, cmd.ptr, cmd.len);
    if r := run(cmd_z) {
        if r.exit_code != 0 { return null; }
        // Strip the trailing newline that `command -v` emits.
        out := r.stdout;
        if out.len > 0 {
            if out[out.len - 1] == 10 { out = substr(out, 0, out.len - 1); }
        }
        if out.len == 0 { return null; }
        return out;
    }
    null;
}
