// Cross-process file locking via an O_EXCL sentinel file.
//
// Target-split layout:
//   lock.mbt         — shared helpers (release, force-remove, TTL parsing,
//                      retry loop). All cross-target via @env / @fs.
//   lock_js.mbt      — JS-target primitives (_try_acquire, _stat_mtime_ms)
//                      + `with_lock` that wraps f() in a JS try/finally
//                      (MoonBit try/catch doesn't see JS-level abort()).
//   lock_native.mbt  — Native-target primitives (extern "C" bindings into
//                      native_stub.c) + `with_lock` that uses MoonBit
//                      `defer` for release.

///| Release the lock file by removing it. Cross-target via @fs.
fn _release(path : String) -> Unit {
  let _ = try? @fs.remove_file(path)

}

///| Current wall-clock time in epoch milliseconds. Cross-target.
fn _now_ms_lock() -> Int64 {
  @env.now().reinterpret_as_int64()
}

///| Best-effort unlink. Returns true if the file was removed or was
///| already gone (ENOENT is treated as success). Cross-target via @fs.
fn _force_remove(path : String) -> Bool {
  if !@fs.path_exists(path) {
    return true
  }
  (try? @fs.remove_file(path)) is Ok(_)
}

///| Read the stale-TTL override from the environment. Returns -1 when
///| unset or invalid so the default is used.
fn _parse_ttl_ms_env() -> Int {
  match @env.get_env_var("MNEMO_LOCK_STALE_TTL_MS") {
    None => -1
    Some(raw) =>
      match (try? @string.parse_int(raw.view())) {
        Ok(n) => if n > 0 { n } else { -1 }
        Err(_) => -1
      }
  }
}

///| Default stale TTL: 30s. A writer holding the sentinel longer than this is
///| almost certainly crashed — memory writes are sub-millisecond atomic rename.
pub let default_lock_stale_ttl_ms : Int = 30000

///| Resolved stale-TTL: env override wins, otherwise default.
fn _effective_stale_ttl_ms() -> Int {
  let env = _parse_ttl_ms_env()
  if env > 0 { env } else { default_lock_stale_ttl_ms }
}

// Cooperative acquire loop lives in the per-target `with_lock` to
// avoid an extra async-fn boundary that the test scheduler sometimes
// mishandles on rapid repeat runs. `with_lock` inlines the retry
// below; shared helpers here are pure / synchronous.

// --- Smoke tests ---

///|
async test "lock: sequential calls succeed" {
  let dir = ffi_tmp_dir_lock()
  let lock_path = dir + "/.writing"
  let mut count = 0
  with_lock(lock_path, fn() { count = count + 1 })
  with_lock(lock_path, fn() { count = count + 1 })
  assert_eq(count, 2)
  ffi_rm_dir_lock(dir)
}

///| Stale sentinel: a writer that crashed left `.writing` behind.
///| Backdating its mtime past the TTL should let the next acquire recover.
async test "lock: stale sentinel is recovered after TTL" {
  let dir = ffi_tmp_dir_lock()
  let lock_path = dir + "/.writing"
  // Create the sentinel as if a crashed writer left it.
  ffi_write_empty_file(lock_path)
  // Backdate far past any reasonable TTL.
  ffi_set_mtime(lock_path, 1000L)
  // Force a short TTL for the test (avoid the 30s default).
  ffi_set_env_lock("MNEMO_LOCK_STALE_TTL_MS", "50")
  let mut count = 0
  with_lock(lock_path, fn() { count = count + 1 })
  assert_eq(count, 1)
  // Sentinel must be gone after with_lock returns.
  assert_eq(ffi_file_exists_lock(lock_path), false)
  ffi_unset_env_lock("MNEMO_LOCK_STALE_TTL_MS")
  ffi_rm_dir_lock(dir)
}

///| Stat / timing primitives used by the recovery path round-trip correctly.
test "lock: mtime and now helpers return sensible values" {
  let dir = ffi_tmp_dir_lock()
  let path = dir + "/probe"
  // Missing file → -1
  assert_eq(_stat_mtime_ms(path), -1L)
  ffi_write_empty_file(path)
  let mtime = _stat_mtime_ms(path)
  let now = _now_ms_lock()
  assert_eq(mtime > 0L, true)
  // A freshly-created file should be no more than a few seconds old.
  assert_eq(now - mtime < 10000L, true)
  ffi_rm_dir_lock(dir)
}

///| force_remove is idempotent.
test "lock: force_remove is idempotent on missing path" {
  let dir = ffi_tmp_dir_lock()
  let path = dir + "/does-not-exist"
  assert_eq(_force_remove(path), true)
  // Still true after a second call.
  assert_eq(_force_remove(path), true)
  ffi_rm_dir_lock(dir)
}

///| Monotonic counter for uniqueness across rapid calls in one process.
let _lock_tmp_counter : Array[Int] = [0]

///| Cross-target scratch dir for lock tests.
fn ffi_tmp_dir_lock() -> String {
  let tmp = (@env.get_env_var("TMPDIR")).unwrap_or("/tmp")
  _lock_tmp_counter[0] = _lock_tmp_counter[0] + 1
  let dir = tmp +
    "/mnemo-lock-" +
    @env.now().to_string() +
    "-" +
    _lock_tmp_counter[0].to_string()
  let _ = try? @fs.create_dir(dir)

  dir
}

///| Cross-target rmdir.
fn ffi_rm_dir_lock(path : String) -> Unit {
  let _ = try? @fs.remove_dir(path)

}

///| Cross-target empty-file writer (simulates a crashed writer sentinel).
fn ffi_write_empty_file(path : String) -> Unit {
  let _ = try? @fs.write_string_to_file(path, "")

}

///| Cross-target file exists check.
fn ffi_file_exists_lock(path : String) -> Bool {
  @fs.path_exists(path)
}