///|
pub struct Lock {
  path : String
}

///|
let lock_ttl_ms : Int64 = 10L * 60L * 1000L

///|
pub async fn acquire_lock(path : String, force? : Bool = false) -> Lock? {
  try @fs.mkdir(path, permission=0o700) catch {
    @os_error.OSError(_) as err if err.is_EEXIST() => {
      if force || lock_is_stale(path) {
        try @fs.rmdir(path, recursive=true) catch {
          _ => return None
        } noraise {
          _ => ()
        }
        return acquire_lock(path, force~)
      }
      return None
    }
    err => raise err
  } noraise {
    _ => ()
  }
  Some({ path, })
}

///|
pub async fn Lock::release(self : Lock) -> Unit {
  try @fs.rmdir(self.path, recursive=true) catch {
    _ => ()
  } noraise {
    _ => ()
  }
}

///|
pub async fn[T] with_lock(
  path : String,
  op : async () -> T,
  force? : Bool = false,
) -> T? {
  let lock = acquire_lock(path, force~)
  guard lock is Some(lock) else { return None }
  try op() catch {
    err => {
      lock.release()
      raise err
    }
  } noraise {
    result => {
      lock.release()
      Some(result)
    }
  }
}

///|
async fn lock_is_stale(path : String) -> Bool {
  let (sec, ns) = @fs.mtime(path)
  let ms = sec * 1000L + Int64::from_int(ns / 1_000_000)
  let now_ms = @env.now().reinterpret_as_int64()
  let age = now_ms - ms
  age > lock_ttl_ms
}