// Native-target primitives and `with_lock` impl. The heavy lifting
// (open(O_EXCL) / stat / unlink / utimes / setenv) happens in C via
// native_stub.c; this file provides the extern "C" bindings and
// wraps release in a MoonBit `defer` so it fires regardless of how
// `f` exits.

///| O_EXCL sentinel acquire. Returns true on fresh create, false if
///| the sentinel already exists.
fn _try_acquire(path : String) -> Bool {
  let bytes = @utf8.encode(path.view())
  mnemo_lock_try_acquire(bytes) == 1
}

///| stat(2) mtime in epoch ms, or -1 on any error / missing file.
fn _stat_mtime_ms(path : String) -> Int64 {
  let bytes = @utf8.encode(path.view())
  mnemo_stat_mtime_ms(bytes)
}

#borrow(path)
extern "C" fn mnemo_lock_try_acquire(path : Bytes) -> Int = "mnemo_lock_try_acquire"

#borrow(path)
extern "C" fn mnemo_stat_mtime_ms(path : Bytes) -> Int64 = "mnemo_stat_mtime_ms"

#borrow(path)
extern "C" fn mnemo_lock_force_remove(path : Bytes) -> Int = "mnemo_lock_force_remove"

#borrow(path, content)
extern "C" fn mnemo_atomic_write_c(path : Bytes, content : Bytes) -> Int = "mnemo_atomic_write"

///| Acquires a lock on `path`, runs `f`, then releases via `defer` so
///| the sentinel is cleaned up even if `f` raises.
pub async fn[T] with_lock(path : String, f : async () -> T) -> T {
  let ttl_ms = _effective_stale_ttl_ms()
  let mut attempts = 0
  while !_try_acquire(path) {
    attempts = attempts + 1
    if attempts % 100 == 0 {
      let mtime = _stat_mtime_ms(path)
      if mtime > 0L {
        let age = _now_ms_lock() - mtime
        if age > ttl_ms.to_int64() {
          let _ = _force_remove(path)
          continue
        }
      }
    }
    let max_attempts = (ttl_ms / 10) + 100
    if attempts > max_attempts {
      abort("lock timeout: " + path)
    }
    sleep_ms(10)
  }
  defer _release(path)
  f()
}

// Test-helper stubs for native. They're unused from tests that actually
// require JS semantics (utimes backdating + env manipulation); if a
// native test run needs them they can be implemented in native_stub.c.

///| utimes(2) shim — not yet implemented for native; the stale-sentinel
///| test is gated to JS. Stubs out to a no-op so the file compiles.
fn ffi_set_mtime(_path : String, _mtime_ms : Int64) -> Unit {
  ()
}

///| setenv(3) shim — no-op on native. The stale-sentinel test reads
///| MNEMO_LOCK_STALE_TTL_MS via @env.get_env_var, so if the test is
///| enabled on native it should be set through the environment
///| before launching the test binary rather than mutated in-process.
fn ffi_set_env_lock(_key : String, _value : String) -> Unit {
  ()
}

///| unsetenv(3) shim — no-op on native. Paired with ffi_set_env_lock.
fn ffi_unset_env_lock(_key : String) -> Unit {
  ()
}