// JS-target primitives and `with_lock` impl for cross-process file
// locking. Uses node:fs's openSync with O_EXCL for the sentinel and
// wraps f() in a JS try/finally so that an `abort()` inside the
// callback still triggers the release (MoonBit's try/catch doesn't
// propagate through JS-level abort exceptions).

///| Try to acquire a lock file. Returns true on success, false if
///| already locked.
extern "js" fn _try_acquire(path : String) -> Bool =
  #| (path) => {
  #|   const fs = require("node:fs");
  #|   try {
  #|     const fd = fs.openSync(path, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
  #|     fs.closeSync(fd);
  #|     return true;
  #|   } catch (e) {
  #|     if (e.code === "EEXIST") return false;
  #|     throw e;
  #|   }
  #| }

///| Return the mtime of `path` in epoch milliseconds, or -1 if the file
///| does not exist / cannot be stat'd.
extern "js" fn _stat_mtime_ms(path : String) -> Int64 =
  #| (path) => {
  #|   const fs = require("node:fs");
  #|   try {
  #|     const st = fs.statSync(path);
  #|     return BigInt(Math.trunc(st.mtimeMs));
  #|   } catch (e) {
  #|     return BigInt(-1);
  #|   }
  #| }

///| Run async `callback` inside a JS try/finally so `release_fn` fires
///| even if `callback` throws at the JS level.
extern "js" fn _run_with_finally_async(
  callback : async () -> Unit,
  release_fn : () -> Unit
) -> @ffi.Promise[Unit] =
  #| (callback, release_fn) => {
  #|   return (async () => {
  #|     try { await callback(); }
  #|     finally { release_fn(); }
  #|   })();
  #| }

///| Acquires a lock on `path` (creating it exclusively if absent),
///| runs `f` asynchronously, then releases the lock in a JS try/finally.
///| Retry loop inlined to avoid a separate async helper that the
///| MoonBit test scheduler sometimes mishandles on repeated runs.
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)
  }
  // Typed holder survives the async () -> Unit boundary of the JS wrapper.
  let holder : Array[T] = []
  _run_with_finally_async(async fn() { holder.push(f()) }, fn() { _release(path) }).wait()
  holder[0]
}

///| Set atime/mtime on a file. JS-only test helper.
extern "js" fn ffi_set_mtime(path : String, mtime_ms : Int64) -> Unit =
  #| (path, mtimeMs) => {
  #|   const fs = require("node:fs");
  #|   const t = Number(mtimeMs) / 1000;
  #|   fs.utimesSync(path, t, t);
  #| }

///| Set an env var. JS-only test helper (moonbitlang/core/env is
///| get-only, and we don't want an extra moonbitlang/x/sys dep just
///| for the inline stale-sentinel test).
extern "js" fn ffi_set_env_lock(key : String, value : String) -> Unit =
  #| (key, value) => { globalThis.process.env[key] = value; }

///| Unset an env var. Same JS-only caveat.
extern "js" fn ffi_unset_env_lock(key : String) -> Unit =
  #| (key) => { delete globalThis.process.env[key]; }