///|
pub(all) struct SpawnSpec {
  bun_path : String
  script : String
  cwd : String
  out_log : String
  err_log : String
  env_blob : Bytes
} derive(Debug)

///|
pub extend SpawnSpec with Debug::{to_repr}

///|
pub fn spawn(spec : SpawnSpec) -> Int raise ProcessError {
  let pid = c_spawn(
    cstr(spec.bun_path),
    cstr(spec.script),
    cstr(spec.cwd),
    cstr(spec.out_log),
    cstr(spec.err_log),
    spec.env_blob,
  )
  if pid < 0 {
    raise Failed(op="spawn", errno=-pid)
  }
  pid
}

///|
pub fn get_pid() -> Int {
  c_getpid()
}

///|
/// Open a pidfd pinned to `pid` (Linux >= 5.3). The fd always refers to
/// that original process, never to a pid-reused successor.
pub fn pidfd_open(pid : Int) -> Int raise ProcessError {
  let fd = c_pidfd_open(pid)
  if fd < 0 {
    raise Failed(op="pidfd_open", errno=-fd)
  }
  fd
}

///|
/// True while the pidfd's process is alive; false once it has exited.
pub fn pidfd_alive(fd : Int) -> Bool {
  c_pidfd_alive(fd) == 1
}

///|
/// True when the process behind the pidfd has really exited. The pidfd
/// becomes readable on exit, but also on stop events; confirming with
/// pid_alive (ESRCH) rules those out. waitid(P_PIDFD) itself only works
/// for child processes, so this poll-based check is the only reliable
/// exit signal for adopted (reparented) instances.
pub fn pidfd_exited(fd : Int, pid : Int) -> Bool {
  !pidfd_alive(fd) && !pid_alive(pid)
}

///|
pub fn sleep_ms(ms : Int) -> Unit {
  c_sleep_ms(ms)
}