#include <compat.h> // IWYU pragma: keep

#include <uv.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <uthash.h>
#include <utarray.h>
#include <errno.h>

#include "ant.h"
#include "ptr.h"
#include "utf8.h"
#include "utils.h"
#include "base64.h"
#include "errors.h"
#include "watch.h"
#include "internal.h"
#include "runtime.h"
#include "descriptors.h"

#include "gc/roots.h"
#include "gc/modules.h"
#include "silver/engine.h"

#include "modules/fs.h"
#include "modules/date.h"
#include "modules/buffer.h"
#include "modules/events.h"
#include "modules/stream.h"
#include "modules/symbol.h"
#include "modules/url.h"

typedef enum {
  FS_ENC_NONE = 0,
  FS_ENC_UTF8,
  FS_ENC_UTF16LE,
  FS_ENC_LATIN1,
  FS_ENC_BASE64,
  FS_ENC_BASE64URL,
  FS_ENC_HEX,
  FS_ENC_ASCII,
} fs_encoding_t;

typedef enum {
  FS_OP_READ,
  FS_OP_WRITE,
  FS_OP_UNLINK,
  FS_OP_MKDIR,
  FS_OP_RMDIR,
  FS_OP_STAT,
  FS_OP_READ_BYTES,
  FS_OP_EXISTS,
  FS_OP_READDIR,
  FS_OP_ACCESS,
  FS_OP_REALPATH,
  FS_OP_WRITE_FD,
  FS_OP_READ_FD,
  FS_OP_OPEN,
  FS_OP_CLOSE,
  FS_OP_MKDTEMP,
  FS_OP_CHMOD,
  FS_OP_RENAME
} fs_op_type_t;

typedef struct fs_request_s {
  uv_fs_t uv_req;
  ant_t *js;
  ant_value_t promise;
  ant_value_t target_buffer;
  ant_value_t callback_fn;

  char *path;
  char *path2;
  char *data;
  char *error_msg;
  size_t data_len;
  size_t buf_offset;

  fs_op_type_t op_type;
  fs_encoding_t encoding;
  uv_file fd;

  int completed;
  int failed;
  int recursive;
  int error_code;
  int with_file_types;
} fs_request_t;

typedef enum {
  FS_WATCH_MODE_EVENT = 0,
  FS_WATCH_MODE_STAT
} fs_watch_mode_t;

typedef union {
  uv_fs_event_t event;
  uv_fs_poll_t poll;
} fs_watcher_handle_t;

typedef struct fs_watcher_s {
  ant_t *js;
  ant_value_t obj;
  ant_value_t callback;
  fs_watcher_handle_t handle;
  uv_stat_t last_stat;
  
  struct fs_watcher_s *next_active;
  char *path;
  
  bool last_stat_valid;
  bool in_active_list;
  bool closing;
  bool handle_closed;
  bool finalized;
  bool persistent;
  
  fs_watch_mode_t mode;
} fs_watcher_t;

typedef struct {
  bool persistent;
  bool recursive;
  ant_value_t listener;
} fs_watch_options_t;

typedef struct {
  bool persistent;
  unsigned int interval_ms;
  ant_value_t listener;
} fs_watchfile_options_t;

typedef struct {
  mode_t mode;
  double size, uid, gid;
  double atime_ms, mtime_ms, ctime_ms, birthtime_ms;
} fs_stat_fields_t;

static ant_value_t g_dirent_proto      = 0;
static ant_value_t g_fswatcher_proto   = 0;
static ant_value_t g_fswatcher_ctor    = 0;
static ant_value_t g_filehandle_proto  = 0;
static ant_value_t g_readstream_proto  = 0;
static ant_value_t g_readstream_ctor   = 0;
static ant_value_t g_writestream_proto = 0;
static ant_value_t g_writestream_ctor  = 0;

static fs_watcher_t *active_watchers = NULL;
static UT_array *pending_requests    = NULL;

enum { 
  FS_WATCHER_NATIVE_TAG = 0x46535754u,   // FSWT
  FS_FILEHANDLE_NATIVE_TAG = 0x46534648u // FSFH
};

static fs_watcher_t *fs_watcher_data(ant_value_t value) {
  if (!js_check_native_tag(value, FS_WATCHER_NATIVE_TAG)) return NULL;
  return (fs_watcher_t *)js_get_native_ptr(value);
}

static void fs_add_active_watcher(fs_watcher_t *watcher) {
  if (!watcher || watcher->in_active_list) return;
  watcher->next_active = active_watchers;
  active_watchers = watcher;
  watcher->in_active_list = true;
}

static void fs_remove_active_watcher(fs_watcher_t *watcher) {
  fs_watcher_t **it = NULL;
  if (!watcher || !watcher->in_active_list) return;

  for (it = &active_watchers; *it; it = &(*it)->next_active) {
    if (*it != watcher) continue;
    *it = watcher->next_active;
    watcher->next_active = NULL;
    watcher->in_active_list = false;
    return;
  }
}

static ant_value_t fs_call_value(
  ant_t *js,
  ant_value_t fn,
  ant_value_t this_val,
  ant_value_t *args,
  int nargs
) {
  ant_value_t saved_this = js->this_val;
  ant_value_t result = js_mkundef();

  js->this_val = this_val;
  if (vtype(fn) == T_CFUNC) result = js_as_cfunc(fn)(js, args, nargs);
  else result = sv_vm_call(js->vm, js, fn, this_val, args, nargs, NULL, false);
  js->this_val = saved_this;
  
  return result;
}

static int parse_open_flags(ant_t *js, ant_value_t arg);
static ant_value_t fs_coerce_path(ant_t *js, ant_value_t arg);

static ant_value_t fs_mk_errno_error(
  ant_t *js, int err_num,
  const char *syscall, const char *path, const char *dest
);

static ant_value_t fs_mk_uv_error(
  ant_t *js, int uv_code,
  const char *syscall, const char *path, const char *dest
);

static bool fs_parse_mode(ant_t *js, ant_value_t arg, mode_t *out) {
  if (vtype(arg) == T_NUM) {
    double mode = js_getnum(arg);
    if (mode < 0) return false;
    *out = (mode_t)mode;
    return true;
  }

  if (vtype(arg) != T_STR) return false;

  size_t len = 0;
  const char *str = js_getstr(js, arg, &len);
  if (!str) return false;

  size_t start = 0;
  if (len > 2 && str[0] == '0' && (str[1] == 'o' || str[1] == 'O')) start = 2;
  if (start == len) return false;

  mode_t mode = 0;
  for (size_t i = start; i < len; i++) {
    if (str[i] < '0' || str[i] > '7') return false;
    mode = (mode_t)((mode << 3) + (str[i] - '0'));
  }

  *out = mode;
  return true;
}

static ant_value_t fs_stream_error(ant_t *js, ant_value_t stream_obj, const char *op, int uv_code) {
  ant_value_t props = js_mkobj(js);
  ant_value_t path_val = js_get(js, stream_obj, "path");
  const char *code = uv_err_name(uv_code);

  if (code) js_set(js, props, "code", js_mkstr(js, code, strlen(code)));
  js_set(js, props, "errno", js_mknum((double)uv_code));
  if (vtype(path_val) == T_STR) js_set(js, props, "path", path_val);
  return js_mkerr_props(js, JS_ERR_TYPE, props, "%s failed: %s", op, uv_strerror(uv_code));
}

static ant_value_t fs_stream_push_chunk(ant_t *js, ant_value_t stream_obj, ant_value_t chunk) {
  return stream_readable_push(js, stream_obj, chunk, js_mkundef());
}

static ant_value_t fs_stream_callback(ant_t *js, ant_value_t callback, ant_value_t value) {
  if (!is_callable(callback)) return js_mkundef();
  return fs_call_value(js, callback, js_mkundef(), &value, 1);
}

static int fs_stream_close_fd_sync(ant_t *js, ant_value_t stream_obj) {
  ant_value_t fd_val = js_get(js, stream_obj, "fd");
  uv_fs_t req;
  int result = 0;

  if (vtype(fd_val) != T_NUM) {
    js_set(js, stream_obj, "pending", js_false);
    js_set(js, stream_obj, "closed", js_true);
    return 0;
  }

  result = uv_fs_close(uv_default_loop(), &req, (uv_file)js_getnum(fd_val), NULL);
  uv_fs_req_cleanup(&req);

  js_set(js, stream_obj, "fd", js_mknull());
  js_set(js, stream_obj, "pending", js_false);
  js_set(js, stream_obj, "closed", js_true);
  
  return result;
}

static int fs_stream_open_fd_sync(ant_t *js, ant_value_t stream_obj) {
  ant_value_t fd_val = js_get(js, stream_obj, "fd");
  ant_value_t path_val = js_get(js, stream_obj, "path");
  ant_value_t mode_val = js_get(js, stream_obj, "mode");
  ant_value_t flags_val = js_get_slot(stream_obj, SLOT_FS_FLAGS);
  
  size_t path_len = 0;
  const char *path = NULL;
  char *path_copy = NULL;
  uv_fs_t req;
  
  int flags = 0;
  int mode = 0666;
  int result = 0;

  if (vtype(fd_val) == T_NUM) return (int)js_getnum(fd_val);
  if (vtype(path_val) != T_STR) return UV_EINVAL;

  path = js_getstr(js, path_val, &path_len);
  if (!path) return UV_EINVAL;

  path_copy = strndup(path, path_len);
  if (!path_copy) return UV_ENOMEM;

  flags = (vtype(flags_val) == T_NUM) ? (int)js_getnum(flags_val) : O_RDONLY;
  if (vtype(mode_val) == T_NUM) mode = (int)js_getnum(mode_val);

  result = uv_fs_open(uv_default_loop(), &req, path_copy, flags, mode, NULL);
  uv_fs_req_cleanup(&req);
  free(path_copy);

  if (result < 0) return result;

  js_set(js, stream_obj, "fd", js_mknum((double)result));
  js_set(js, stream_obj, "pending", js_false);
  js_set(js, stream_obj, "closed", js_false);

  ant_value_t open_arg = js_mknum((double)result);
  eventemitter_emit_args(js, stream_obj, "open", &open_arg, 1);
  eventemitter_emit_args(js, stream_obj, "ready", NULL, 0);

  return result;
}

static ant_value_t fs_stream_destroy(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t stream_obj = js_getthis(js);
  ant_value_t err = nargs > 0 ? args[0] : js_mknull();
  ant_value_t callback = nargs > 1 ? args[1] : js_mkundef();
  int result = fs_stream_close_fd_sync(js, stream_obj);

  if (result < 0 && (is_null(err) || is_undefined(err)))
    err = fs_stream_error(js, stream_obj, "close", result);
  if (is_undefined(err)) err = js_mknull();

  return fs_stream_callback(js, callback, err);
}

static ant_value_t fs_stream_close(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t stream_obj = js_getthis(js);

  if (nargs > 0 && is_callable(args[0])) {
    eventemitter_add_listener(js, stream_obj, "close", args[0], true);
    if (js_truthy(js, js_get(js, stream_obj, "closed")))
      fs_call_value(js, args[0], js_mkundef(), NULL, 0);
  }

  if (!js_truthy(js, js_get(js, stream_obj, "destroyed"))) {
    ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy");
    if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, NULL, 0);
  }

  return stream_obj;
}

static ant_value_t fs_readstream__read(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t stream_obj = js_getthis(js);
  ant_value_t pos_val = js_get(js, stream_obj, "pos");
  ant_value_t end_val = js_get(js, stream_obj, "end");
  ant_value_t bytes_read_val = js_get(js, stream_obj, "bytesRead");
  
  int fd = fs_stream_open_fd_sync(js, stream_obj);
  int64_t pos = (vtype(pos_val) == T_NUM) ? (int64_t)js_getnum(pos_val) : 0;
  int64_t end = (vtype(end_val) == T_NUM) ? (int64_t)js_getnum(end_val) : -1;
  
  size_t want = 16384;
  bool reached_eof = false;

  if (fd < 0) {
    ant_value_t err = fs_stream_error(js, stream_obj, "open", fd);
    ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy");
    if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, &err, 1);
    return js_mkundef();
  }

  if (nargs > 0 && vtype(args[0]) == T_NUM && js_getnum(args[0]) > 0)
    want = (size_t)js_getnum(args[0]);

  if (end >= 0) {
    if (pos > end) {
      if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj);
      return fs_stream_push_chunk(js, stream_obj, js_mknull());
    }
    if ((int64_t)want > (end - pos + 1)) want = (size_t)(end - pos + 1);
  }

  ArrayBufferData *ab = create_array_buffer_data(want);
  if (!ab) {
    ant_value_t err = js_mkerr(js, "Failed to allocate ReadStream buffer");
    ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy");
    if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, &err, 1);
    return js_mkundef();
  }

  uv_fs_t req;
  uv_buf_t buf = uv_buf_init((char *)ab->data, (unsigned int)want);
  int result = uv_fs_read(uv_default_loop(), &req, fd, &buf, 1, pos, NULL);
  uv_fs_req_cleanup(&req);

  if (result < 0) {
    ant_value_t err = fs_stream_error(js, stream_obj, "read", result);
    ant_value_t destroy_fn = js_getprop_fallback(js, stream_obj, "destroy");
    free_array_buffer_data(ab);
    if (is_callable(destroy_fn)) fs_call_value(js, destroy_fn, stream_obj, &err, 1);
    return js_mkundef();
  }

  if (result == 0) {
    free_array_buffer_data(ab);
    if (js_truthy(js, js_get(js, stream_obj, "autoClose")))
      (void)fs_stream_close_fd_sync(js, stream_obj);
    return fs_stream_push_chunk(js, stream_obj, js_mknull());
  }

  ant_value_t chunk = create_typed_array(js, TYPED_ARRAY_UINT8, ab, 0, (size_t)result, "Buffer");
  if (vtype(chunk) == T_ERR) {
    free_array_buffer_data(ab);
    return chunk;
  }

  if (result < (int)want) reached_eof = true;
  if (end >= 0 && (pos + result - 1) >= end) reached_eof = true;

  js_set(js, stream_obj, "pos", js_mknum((double)(pos + result)));
  js_set(js, stream_obj, "bytesRead", js_mknum(
    (vtype(bytes_read_val) == T_NUM 
    ? js_getnum(bytes_read_val) : 0.0) + (double)result
  ));
  
  fs_stream_push_chunk(js, stream_obj, chunk);

  if (reached_eof) {
    if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj);
    return fs_stream_push_chunk(js, stream_obj, js_mknull());
  }

  return js_mkundef();
}

static ant_value_t fs_writestream__write(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t stream_obj = js_getthis(js);
  ant_value_t callback = nargs > 2 ? args[2] : js_mkundef();
  ant_value_t pos_val = js_get(js, stream_obj, "pos");
  ant_value_t bytes_written_val = js_get(js, stream_obj, "bytesWritten");
  
  const uint8_t *bytes = NULL;
  size_t len = 0;
  
  int fd = fs_stream_open_fd_sync(js, stream_obj);
  int64_t pos = (vtype(pos_val) == T_NUM) ? (int64_t)js_getnum(pos_val) : -1;
  size_t offset = 0;

  if (fd < 0) return fs_stream_callback(js, callback, fs_stream_error(js, stream_obj, "open", fd));

  if (vtype(args[0]) == T_STR) {
    bytes = (const uint8_t *)js_getstr(js, args[0], &len);
  } else if (!buffer_source_get_bytes(js, args[0], &bytes, &len)) {
    return fs_stream_callback(js, callback, js_mkerr(js, "WriteStream chunk must be a string or ArrayBufferView"));
  }

  while (offset < len) {
    uv_fs_t req;
    uv_buf_t buf = uv_buf_init((char *)(bytes + offset), (unsigned int)(len - offset));
    int result = uv_fs_write(uv_default_loop(), &req, fd, &buf, 1, pos, NULL);
    uv_fs_req_cleanup(&req);

    if (result < 0) {
      if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj);
      return fs_stream_callback(js, callback, fs_stream_error(js, stream_obj, "write", result));
    }
    if (result == 0) {
      if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) fs_stream_close_fd_sync(js, stream_obj);
      return fs_stream_callback(js, callback, js_mkerr(js, "write failed: short write"));
    }

    offset += (size_t)result;
    if (pos >= 0) pos += result;
  }

  if (pos >= 0) js_set(js, stream_obj, "pos", js_mknum((double)pos));
  js_set(js, stream_obj, "bytesWritten", js_mknum(
    (vtype(bytes_written_val) == T_NUM 
    ? js_getnum(bytes_written_val) : 0.0) + (double)offset
  ));
  
  return fs_stream_callback(js, callback, js_mknull());
}

static ant_value_t fs_writestream__final(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t stream_obj = js_getthis(js);
  ant_value_t callback = nargs > 0 ? args[0] : js_mkundef();
  ant_value_t value = js_mknull();

  if (js_truthy(js, js_get(js, stream_obj, "autoClose"))) {
    int result = fs_stream_close_fd_sync(js, stream_obj);
    if (result < 0) value = fs_stream_error(js, stream_obj, "close", result);
  }

  return fs_stream_callback(js, callback, value);
}

static ant_value_t fs_create_readstream_impl(ant_t *js, ant_value_t path_arg, ant_value_t options_arg, ant_value_t proto) {
  ant_value_t path_val = fs_coerce_path(js, path_arg);
  ant_value_t options = is_object_type(options_arg) ? options_arg : js_mkobj(js);
  ant_value_t stream_options = js_mkobj(js);
  
  ant_value_t hwm = js_get(js, options, "highWaterMark");
  ant_value_t flags_raw = js_get(js, options, "flags");
  ant_value_t fd_val = js_get(js, options, "fd");
  ant_value_t start_val = js_get(js, options, "start");
  ant_value_t end_val = js_get(js, options, "end");
  ant_value_t mode_val = js_get(js, options, "mode");
  
  ant_value_t auto_close_val = js_get(js, options, "autoClose");
  ant_value_t emit_close_val = js_get(js, options, "emitClose");
  ant_value_t stream_obj = 0;
  
  int flags = parse_open_flags(js, is_undefined(flags_raw) ? js_mkstr(js, "r", 1) : flags_raw);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "ReadStream path must be a string");
  if (vtype(hwm) == T_NUM && js_getnum(hwm) > 0) js_set(js, stream_options, "highWaterMark", hwm);

  stream_obj = stream_construct_readable(js, proto, stream_options);
  if (is_err(stream_obj)) return stream_obj;

  js_set(js, stream_obj, "_read", js_mkfun(fs_readstream__read));
  js_set(js, stream_obj, "_destroy", js_mkfun(fs_stream_destroy));
  js_set(js, stream_obj, "path", path_val);
  js_set(js, stream_obj, "flags", is_undefined(flags_raw) ? js_mkstr(js, "r", 1) : flags_raw);
  js_set(js, stream_obj, "mode", vtype(mode_val) == T_NUM ? mode_val : js_mknum(0666));
  js_set(js, stream_obj, "fd", vtype(fd_val) == T_NUM ? fd_val : js_mkundef());
  js_set(js, stream_obj, "pending", js_bool(vtype(fd_val) != T_NUM));
  js_set(js, stream_obj, "closed", js_false);
  js_set(js, stream_obj, "autoClose", is_undefined(auto_close_val) ? js_true : js_bool(js_truthy(js, auto_close_val)));
  js_set(js, stream_obj, "emitClose", is_undefined(emit_close_val) ? js_true : js_bool(js_truthy(js, emit_close_val)));
  js_set(js, stream_obj, "bytesRead", js_mknum(0));
  js_set(js, stream_obj, "start", vtype(start_val) == T_NUM ? start_val : js_mkundef());
  js_set(js, stream_obj, "end", vtype(end_val) == T_NUM ? end_val : js_mkundef());
  js_set(js, stream_obj, "pos", js_mknum(vtype(start_val) == T_NUM ? js_getnum(start_val) : 0.0));
  js_set_slot(stream_obj, SLOT_FS_FLAGS, js_mknum((double)flags));
  
  return stream_obj;
}

static ant_value_t fs_create_writestream_impl(ant_t *js, ant_value_t path_arg, ant_value_t options_arg, ant_value_t proto) {
  ant_value_t path_val = fs_coerce_path(js, path_arg);
  ant_value_t options = is_object_type(options_arg) ? options_arg : js_mkobj(js);
  ant_value_t stream_options = js_mkobj(js);
  
  ant_value_t flags_raw = js_get(js, options, "flags");
  ant_value_t fd_val = js_get(js, options, "fd");
  ant_value_t start_val = js_get(js, options, "start");
  ant_value_t mode_val = js_get(js, options, "mode");
  ant_value_t auto_close_val = js_get(js, options, "autoClose");
  ant_value_t emit_close_val = js_get(js, options, "emitClose");
  ant_value_t hwm = js_get(js, options, "highWaterMark");
  ant_value_t stream_obj = 0;
  
  int flags = parse_open_flags(js, is_undefined(flags_raw) ? js_mkstr(js, "w", 1) : flags_raw);
  double start_pos = (vtype(start_val) == T_NUM) ? js_getnum(start_val) : ((flags & O_APPEND) ? -1.0 : 0.0);

  if (vtype(path_val) != T_STR) return js_mkerr(js, "WriteStream path must be a string");
  if (vtype(hwm) == T_NUM && js_getnum(hwm) > 0) js_set(js, stream_options, "highWaterMark", hwm);

  stream_obj = stream_construct_writable(js, proto, stream_options);
  if (is_err(stream_obj)) return stream_obj;

  js_set(js, stream_obj, "_write", js_mkfun(fs_writestream__write));
  js_set(js, stream_obj, "_final", js_mkfun(fs_writestream__final));
  js_set(js, stream_obj, "_destroy", js_mkfun(fs_stream_destroy));
  js_set(js, stream_obj, "path", path_val);
  js_set(js, stream_obj, "flags", is_undefined(flags_raw) ? js_mkstr(js, "w", 1) : flags_raw);
  js_set(js, stream_obj, "mode", vtype(mode_val) == T_NUM ? mode_val : js_mknum(0666));
  js_set(js, stream_obj, "fd", vtype(fd_val) == T_NUM ? fd_val : js_mkundef());
  js_set(js, stream_obj, "pending", js_bool(vtype(fd_val) != T_NUM));
  js_set(js, stream_obj, "closed", js_false);
  js_set(js, stream_obj, "autoClose", is_undefined(auto_close_val) ? js_true : js_bool(js_truthy(js, auto_close_val)));
  js_set(js, stream_obj, "emitClose", is_undefined(emit_close_val) ? js_true : js_bool(js_truthy(js, emit_close_val)));
  js_set(js, stream_obj, "bytesWritten", js_mknum(0));
  js_set(js, stream_obj, "start", vtype(start_val) == T_NUM ? start_val : js_mkundef());
  js_set(js, stream_obj, "pos", js_mknum(start_pos));
  js_set_slot(stream_obj, SLOT_FS_FLAGS, js_mknum((double)flags));
  
  return stream_obj;
}

static ant_value_t js_readstream_ctor(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "ReadStream() requires a path argument");
  return fs_create_readstream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_readstream_proto);
}

static ant_value_t js_writestream_ctor(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "WriteStream() requires a path argument");
  return fs_create_writestream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_writestream_proto);
}

static ant_value_t builtin_fs_createReadStream(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "createReadStream() requires a path argument");
  return fs_create_readstream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_readstream_proto);
}

static ant_value_t builtin_fs_createWriteStream(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "createWriteStream() requires a path argument");
  return fs_create_writestream_impl(js, args[0], nargs > 1 ? args[1] : js_mkundef(), g_writestream_proto);
}

static void fs_init_stream_constructors(ant_t *js) {
  if (g_readstream_ctor && g_writestream_ctor) return;

  stream_init_constructors(js);

  g_readstream_proto = js_mkobj(js);
  js_set_proto_init(g_readstream_proto, stream_readable_prototype(js));
  js_set(js, g_readstream_proto, "close", js_mkfun(fs_stream_close));
  js_set_sym(js, g_readstream_proto, get_toStringTag_sym(), js_mkstr(js, "ReadStream", 10));
  g_readstream_ctor = js_make_ctor(js, js_readstream_ctor, g_readstream_proto, "ReadStream", 10);
  js_set_proto_init(g_readstream_ctor, stream_readable_constructor(js));

  g_writestream_proto = js_mkobj(js);
  js_set_proto_init(g_writestream_proto, stream_writable_prototype(js));
  js_set(js, g_writestream_proto, "close", js_mkfun(fs_stream_close));
  js_set_sym(js, g_writestream_proto, get_toStringTag_sym(), js_mkstr(js, "WriteStream", 11));
  g_writestream_ctor = js_make_ctor(js, js_writestream_ctor, g_writestream_proto, "WriteStream", 11);
  js_set_proto_init(g_writestream_ctor, stream_writable_constructor(js));
}

static ant_value_t fs_make_date(ant_t *js, double ms) {
  ant_value_t obj = js_mkobj(js);
  ant_value_t date_proto = js_get_ctor_proto(js, "Date", 4);
  if (is_object_type(date_proto)) js_set_proto_init(obj, date_proto);
  js_set_slot(obj, SLOT_DATA, tov(ms));
  js_set_slot(obj, SLOT_BRAND, js_mknum(BRAND_DATE));
  return obj;
}

static ant_value_t fs_stats_object_new(ant_t *js, const fs_stat_fields_t *f) {
  ant_value_t stat_obj = js_mkobj(js);
  ant_value_t proto = js_get_ctor_proto(js, "Stats", 5);

  if (is_object_type(proto) || is_special_object(proto))
    js_set_proto_init(stat_obj, proto);

  js_set_slot(stat_obj, SLOT_DATA, js_mknum((double)f->mode));
  js_set(js, stat_obj, "size", js_mknum(f->size));
  js_set(js, stat_obj, "mode", js_mknum((double)f->mode));
  js_set(js, stat_obj, "uid", js_mknum(f->uid));
  js_set(js, stat_obj, "gid", js_mknum(f->gid));

  js_set(js, stat_obj, "atimeMs",      js_mknum(f->atime_ms));
  js_set(js, stat_obj, "mtimeMs",      js_mknum(f->mtime_ms));
  js_set(js, stat_obj, "ctimeMs",      js_mknum(f->ctime_ms));
  js_set(js, stat_obj, "birthtimeMs",  js_mknum(f->birthtime_ms));

  js_set(js, stat_obj, "atime",      fs_make_date(js, f->atime_ms));
  js_set(js, stat_obj, "mtime",      fs_make_date(js, f->mtime_ms));
  js_set(js, stat_obj, "ctime",      fs_make_date(js, f->ctime_ms));
  js_set(js, stat_obj, "birthtime",  fs_make_date(js, f->birthtime_ms));
  
  return stat_obj;
}

static double uv_ts_to_ms(uv_timespec_t ts) {
  return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
}

static double posix_ts_to_ms(struct timespec ts) {
  return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
}

#ifdef _WIN32
  #define POSIX_ATIME_MS(st) ((double)(st)->st_atime * 1000.0)
  #define POSIX_MTIME_MS(st) ((double)(st)->st_mtime * 1000.0)
  #define POSIX_CTIME_MS(st) ((double)(st)->st_ctime * 1000.0)
  #define POSIX_BIRTH_MS(st) 0.0
#elif defined(__APPLE__)
  #define POSIX_ATIME_MS(st) posix_ts_to_ms((st)->st_atimespec)
  #define POSIX_MTIME_MS(st) posix_ts_to_ms((st)->st_mtimespec)
  #define POSIX_CTIME_MS(st) posix_ts_to_ms((st)->st_ctimespec)
  #define POSIX_BIRTH_MS(st) posix_ts_to_ms((st)->st_birthtimespec)
#elif defined(__linux__)
  #define POSIX_ATIME_MS(st) posix_ts_to_ms((st)->st_atim)
  #define POSIX_MTIME_MS(st) posix_ts_to_ms((st)->st_mtim)
  #define POSIX_CTIME_MS(st) posix_ts_to_ms((st)->st_ctim)
  #define POSIX_BIRTH_MS(st) 0.0
#else
  #define POSIX_ATIME_MS(st) posix_ts_to_ms((st)->st_atim)
  #define POSIX_MTIME_MS(st) posix_ts_to_ms((st)->st_mtim)
  #define POSIX_CTIME_MS(st) posix_ts_to_ms((st)->st_ctim)
  #define POSIX_BIRTH_MS(st) 0.0
#endif

static const fs_stat_fields_t fs_stat_fields_zero = {0};

static ant_value_t fs_stats_object_from_uv(ant_t *js, const uv_stat_t *st) {
  if (!st) return fs_stats_object_new(js, &fs_stat_fields_zero);
  return fs_stats_object_new(js, &(fs_stat_fields_t){
    .mode         = (mode_t)st->st_mode,
    .size         = (double)st->st_size,
    .uid          = (double)st->st_uid,
    .gid          = (double)st->st_gid,
    .atime_ms     = uv_ts_to_ms(st->st_atim),
    .mtime_ms     = uv_ts_to_ms(st->st_mtim),
    .ctime_ms     = uv_ts_to_ms(st->st_ctim),
    .birthtime_ms = uv_ts_to_ms(st->st_birthtim),
  });
}

static ant_value_t fs_stats_object_from_posix(ant_t *js, const struct stat *st) {
  if (!st) return fs_stats_object_new(js, &fs_stat_fields_zero);
  return fs_stats_object_new(js, &(fs_stat_fields_t){
    .mode         = st->st_mode,
    .size         = (double)st->st_size,
    .uid          = (double)st->st_uid,
    .gid          = (double)st->st_gid,
    .atime_ms     = POSIX_ATIME_MS(st),
    .mtime_ms     = POSIX_MTIME_MS(st),
    .ctime_ms     = POSIX_CTIME_MS(st),
    .birthtime_ms = POSIX_BIRTH_MS(st),
  });
}

static bool fs_stat_path_sync(const char *path, uv_stat_t *out) {
  uv_fs_t req;
  int rc = 0;

  if (!path || !out) return false;
  memset(out, 0, sizeof(*out));

  rc = uv_fs_stat(NULL, &req, path, NULL);
  if (rc < 0) {
    uv_fs_req_cleanup(&req);
    return false;
  }

  *out = req.statbuf;
  uv_fs_req_cleanup(&req);
  return true;
}

static const char *fs_watch_basename(const char *path) {
  const char *name = NULL;

  if (!path || !*path) return NULL;
  name = strrchr(path, '/');
#ifdef _WIN32
  {
    const char *alt = strrchr(path, '\\');
    if (!name || (alt && alt > name)) name = alt;
  }
#endif
  return name ? name + 1 : path;
}

static ant_value_t fs_watch_error(ant_t *js, int status, const char *path) {
  ant_value_t props = js_mkobj(js);
  const char *code = uv_err_name(status);
  js_set(js, props, "code", js_mkstr(js, code, strlen(code)));
  
  return js_mkerr_props(
    js, JS_ERR_TYPE,
    props, "%s: %s",
    path ? path : "watch",
    uv_strerror(status)
  );
}

static void fs_watcher_free(fs_watcher_t *watcher) {
  if (!watcher) return;
  free(watcher->path);
  free(watcher);
}

static uv_handle_t *fs_watcher_uv_handle(fs_watcher_t *watcher) {
  if (!watcher) return NULL;
  if (watcher->mode == FS_WATCH_MODE_STAT) return (uv_handle_t *)&watcher->handle.poll;
  return (uv_handle_t *)&watcher->handle.event;
}

static void fs_watcher_on_handle_closed(uv_handle_t *handle) {
  fs_watcher_t *watcher = (fs_watcher_t *)handle->data;

  if (!watcher) return;
  watcher->handle_closed = true;
  watcher->closing = false;
  if (watcher->finalized) fs_watcher_free(watcher);
}

static void fs_watcher_close_native(fs_watcher_t *watcher) {
  uv_handle_t *handle = NULL;

  if (!watcher) return;
  if (watcher->closing || watcher->handle_closed) return;

  handle = fs_watcher_uv_handle(watcher);
  if (!handle) return;

  fs_remove_active_watcher(watcher);
  if (watcher->mode == FS_WATCH_MODE_STAT) uv_fs_poll_stop(&watcher->handle.poll);
  else ant_watch_stop(&watcher->handle.event);
  watcher->closing = true;
  uv_close(handle, fs_watcher_on_handle_closed);
}

static void fs_watcher_emit_error(fs_watcher_t *watcher, int status) {
  ant_value_t args[1];

  if (!watcher || vtype(watcher->obj) != T_OBJ) return;
  args[0] = fs_watch_error(watcher->js, status, watcher->path);
  eventemitter_emit_args(watcher->js, watcher->obj, "error", args, 1);
}

static void fs_watcher_emit_change(fs_watcher_t *watcher, const char *filename, int events) {
  ant_value_t args[2];
  const char *event_name = "change";
  const char *name = filename;

  if (!watcher || vtype(watcher->obj) != T_OBJ) return;
  if ((events & UV_RENAME) != 0) event_name = "rename";
  if (!name || !*name) name = fs_watch_basename(watcher->path);

  args[0] = js_mkstr(watcher->js, event_name, strlen(event_name));
  args[1] = name ? js_mkstr(watcher->js, name, strlen(name)) : js_mkundef();
  eventemitter_emit_args(watcher->js, watcher->obj, "change", args, 2);
}

static void fs_watcher_invoke_watchfile_stats(
  fs_watcher_t *watcher,
  const uv_stat_t *curr,
  const uv_stat_t *prev
) {
  uv_stat_t curr_stat;
  uv_stat_t prev_stat;
  ant_value_t args[2];

  if (!watcher || !is_callable(watcher->callback)) return;

  memset(&curr_stat, 0, sizeof(curr_stat));
  memset(&prev_stat, 0, sizeof(prev_stat));
  if (curr) curr_stat = *curr;
  if (prev) prev_stat = *prev;

  watcher->last_stat = curr_stat;
  watcher->last_stat_valid = curr != NULL;

  args[0] = fs_stats_object_from_uv(watcher->js, &curr_stat);
  args[1] = fs_stats_object_from_uv(watcher->js, &prev_stat);
  fs_call_value(watcher->js, watcher->callback, js_mkundef(), args, 2);
}

static void fs_watcher_on_event(
  uv_fs_event_t *handle,
  const char *filename,
  int events,
  int status
) {
  fs_watcher_t *watcher = (fs_watcher_t *)handle->data;

  if (!watcher) return;
  if (status < 0) {
    fs_watcher_emit_error(watcher, status);
    return;
  }

  if ((events & (UV_CHANGE | UV_RENAME)) == 0) return;
  fs_watcher_emit_change(watcher, filename, events);
}

static void fs_watcher_on_poll(
  uv_fs_poll_t *handle,
  int status,
  const uv_stat_t *prev,
  const uv_stat_t *curr
) {
  fs_watcher_t *watcher = (fs_watcher_t *)handle->data;
  uv_stat_t missing = {0};

  if (!watcher) return;
  if (status < 0 && status != UV_ENOENT) return;
  if (status == UV_ENOENT) {
    if (watcher->last_stat_valid) fs_watcher_invoke_watchfile_stats(watcher, &missing, &watcher->last_stat);
    else fs_watcher_invoke_watchfile_stats(watcher, &missing, &missing);
    return;
  }

  fs_watcher_invoke_watchfile_stats(watcher, curr, prev);
}

static ant_value_t js_fswatcher_close(ant_t *js, ant_value_t *args, int nargs) {
  fs_watcher_t *watcher = fs_watcher_data(js->this_val);

  if (!watcher) return js->this_val;
  fs_watcher_close_native(watcher);
  return js->this_val;
}

static ant_value_t js_fswatcher_ref(ant_t *js, ant_value_t *args, int nargs) {
  fs_watcher_t *watcher = fs_watcher_data(js->this_val);
  uv_handle_t *handle = NULL;

  if (!watcher || watcher->handle_closed) return js->this_val;
  handle = fs_watcher_uv_handle(watcher);
  if (handle) uv_ref(handle);
  watcher->persistent = true;
  return js->this_val;
}

static ant_value_t js_fswatcher_unref(ant_t *js, ant_value_t *args, int nargs) {
  fs_watcher_t *watcher = fs_watcher_data(js->this_val);
  uv_handle_t *handle = NULL;

  if (!watcher || watcher->handle_closed) return js->this_val;
  handle = fs_watcher_uv_handle(watcher);
  if (handle) uv_unref(handle);
  watcher->persistent = false;
  return js->this_val;
}

static ant_value_t js_fswatcher_ctor(ant_t *js, ant_value_t *args, int nargs) {
  return js_mkerr_typed(js, JS_ERR_TYPE, "FSWatcher cannot be constructed directly");
}

static void fs_watcher_finalize(ant_t *js, ant_object_t *obj) {
  fs_watcher_t *watcher = NULL;
  if (!obj) return;

  watcher = (fs_watcher_t *)obj->native.ptr;
  obj->native.ptr = NULL;
  if (!watcher) return;

  watcher->finalized = true;
  fs_watcher_close_native(watcher);
  if (watcher->handle_closed) fs_watcher_free(watcher);
}

static void fs_init_watch_constructors(ant_t *js) {
  ant_value_t events = 0;
  ant_value_t ee_ctor = 0;
  ant_value_t ee_proto = 0;

  if (g_fswatcher_proto && g_fswatcher_ctor) return;

  events = events_library(js);
  ee_ctor = js_get(js, events, "EventEmitter");
  ee_proto = js_get(js, ee_ctor, "prototype");

  g_fswatcher_proto = js_mkobj(js);
  js_set_proto_init(g_fswatcher_proto, ee_proto);
  
  js_set(js, g_fswatcher_proto, "close", js_mkfun(js_fswatcher_close));
  js_set(js, g_fswatcher_proto, "ref", js_mkfun(js_fswatcher_ref));
  js_set(js, g_fswatcher_proto, "unref", js_mkfun(js_fswatcher_unref));
  
  js_set_sym(js, g_fswatcher_proto, get_toStringTag_sym(), js_mkstr(js, "FSWatcher", 9));
  g_fswatcher_ctor = js_make_ctor(js, js_fswatcher_ctor, g_fswatcher_proto, "FSWatcher", 9);
}

static bool fs_parse_watch_options(ant_t *js, ant_value_t *args, int nargs, fs_watch_options_t *out) {
  ant_value_t options = js_mkundef();
  ant_value_t persistent_val = js_mkundef();

  if (!out) return false;

  memset(out, 0, sizeof(*out));
  out->persistent = true;
  out->listener = js_mkundef();

  if (nargs > 1) {
    if (is_callable(args[1])) out->listener = args[1];
    else options = args[1];
  }

  if (nargs > 2 && is_callable(args[2])) out->listener = args[2];
  if (vtype(options) == T_UNDEF || vtype(options) == T_NULL || vtype(options) == T_STR) return true;
  if (vtype(options) != T_OBJ) return false;

  persistent_val = js_get(js, options, "persistent");
  if (vtype(persistent_val) != T_UNDEF) out->persistent = js_truthy(js, persistent_val);
  out->recursive = js_truthy(js, js_get(js, options, "recursive"));
  return true;
}

static bool fs_parse_watchfile_options(
  ant_t *js,
  ant_value_t *args,
  int nargs,
  fs_watchfile_options_t *out
) {
  ant_value_t options = js_mkundef();
  ant_value_t persistent_val = js_mkundef();

  if (!out) return false;

  memset(out, 0, sizeof(*out));
  out->persistent = true;
  out->interval_ms = 5007;
  out->listener = js_mkundef();

  if (nargs > 1) {
    if (is_callable(args[1])) {
      out->listener = args[1];
      return true;
    }
    options = args[1];
  }

  if (nargs > 2 && is_callable(args[2])) out->listener = args[2];
  if (!is_callable(out->listener)) return false;
  if (vtype(options) == T_UNDEF || vtype(options) == T_NULL) return true;
  if (vtype(options) != T_OBJ) return false;

  persistent_val = js_get(js, options, "persistent");
  if (vtype(persistent_val) != T_UNDEF) out->persistent = js_truthy(js, persistent_val);
  {
    ant_value_t interval_val = js_get(js, options, "interval");
    if (vtype(interval_val) == T_NUM && js_getnum(interval_val) > 0)
      out->interval_ms = (unsigned int)js_getnum(interval_val);
  }
  return true;
}

static fs_watcher_t *fs_watcher_new(ant_t *js, fs_watch_mode_t mode) {
  fs_watcher_t *watcher = calloc(1, sizeof(*watcher));

  if (!watcher) return NULL;
  watcher->js = js;
  watcher->obj = js_mkundef();
  watcher->callback = js_mkundef();
  watcher->persistent = true;
  watcher->mode = mode;
  
  return watcher;
}

static int fs_watcher_start_event(fs_watcher_t *watcher, const char *path, bool persistent, bool recursive) {
  unsigned int flags = 0;

  if (!watcher || !path) return UV_EINVAL;
#ifdef UV_FS_EVENT_RECURSIVE
  if (recursive) flags |= UV_FS_EVENT_RECURSIVE;
#else
  (void)recursive;
#endif

  watcher->persistent = persistent;
  return ant_watch_start(
    uv_default_loop(),
    &watcher->handle.event,
    path, fs_watcher_on_event,
    watcher, flags, &watcher->path
  );
}

static int fs_watcher_start_poll(fs_watcher_t *watcher, const char *path, bool persistent, unsigned int interval_ms) {
  int rc = 0;

  if (!watcher || !path) return UV_EINVAL;

  watcher->path = ant_watch_resolve_path(path);
  if (!watcher->path) return UV_ENOMEM;

  rc = uv_fs_poll_init(uv_default_loop(), &watcher->handle.poll);
  if (rc != 0) goto fail;

  watcher->handle.poll.data = watcher;
  rc = uv_fs_poll_start(&watcher->handle.poll, fs_watcher_on_poll, watcher->path, interval_ms);
  if (rc != 0) goto fail;

  watcher->persistent = persistent;
  return 0;

fail:
  free(watcher->path);
  watcher->path = NULL;
  return rc;
}

static ant_value_t fs_watcher_make_object(ant_t *js, fs_watcher_t *watcher) {
  ant_value_t obj = 0;

  if (!watcher) return js_mkerr(js, "Out of memory");
  fs_init_watch_constructors(js);

  obj = js_mkobj(js);
  js_set_proto_init(obj, g_fswatcher_proto);
  js_set_native_ptr(obj, watcher);
  js_set_native_tag(obj, FS_WATCHER_NATIVE_TAG);
  js_set_finalizer(obj, fs_watcher_finalize);
  watcher->obj = obj;
  
  return obj;
}

static void fs_request_fail(fs_request_t *req, int uv_code) {
  req->failed = 1;
  req->error_code = uv_code;
  if (req->error_msg) free(req->error_msg);
  req->error_msg = strdup(uv_strerror(uv_code));
}

static ant_value_t fs_coerce_file_url_path(ant_t *js, ant_value_t arg) {
  ant_value_t href = js_getprop_fallback(js, arg, "href");
  if (vtype(href) != T_STR) return js_mkundef();

  const char *href_str = js_getstr(js, href, NULL);
  if (!href_str) return js_mkundef();

  url_state_t parsed = {0};
  if (parse_url_to_state(href_str, NULL, &parsed) != 0) return js_mkundef();

  ant_value_t path = js_mkundef();
  if (parsed.protocol && strcmp(parsed.protocol, "file:") == 0) {
  char *decoded = url_decode_component(parsed.pathname);
  if (decoded) {
    path = js_mkstr(js, decoded, strlen(decoded));
    free(decoded);
  }}

  url_state_clear(&parsed);
  return path;
}

static ant_value_t fs_coerce_path(ant_t *js, ant_value_t arg) {
  if (vtype(arg) == T_STR) return arg;
  if (!is_object_type(arg)) return js_mkundef();

  ant_value_t path = fs_coerce_file_url_path(js, arg);
  if (!is_undefined(path)) return path;

  path = js_get(js, arg, "pathname");
  if (vtype(path) == T_STR) return path;

  return js_mkundef();
}

static int fs_remove_path_recursive(const char *path, bool recursive, bool force) {
  uv_fs_t req;
  
  int result = uv_fs_lstat(NULL, &req, path, NULL);
  if (result < 0) {
    uv_fs_req_cleanup(&req);
    return (force && result == UV_ENOENT) ? 0 : result;
  }

  uv_stat_t statbuf = req.statbuf;
  uv_fs_req_cleanup(&req);

  if ((statbuf.st_mode & S_IFMT) != S_IFDIR) {
    result = uv_fs_unlink(NULL, &req, path, NULL);
    uv_fs_req_cleanup(&req);
    return (force && result == UV_ENOENT) ? 0 : result;
  }

  if (!recursive) return UV_EISDIR;
  result = uv_fs_scandir(NULL, &req, path, 0, NULL);
  
  if (result < 0) {
    uv_fs_req_cleanup(&req);
    return (force && result == UV_ENOENT) ? 0 : result;
  }

  for (;;) {
    uv_dirent_t entry;
    result = uv_fs_scandir_next(&req, &entry);
    if (result == UV_EOF) break;
    if (result < 0) {
      uv_fs_req_cleanup(&req);
      return result;
    }
    
    size_t path_len = strlen(path);
    size_t name_len = strlen(entry.name);
    
    bool needs_sep = path_len > 0 && path[path_len - 1] != '/';
    char *child = malloc(path_len + (needs_sep ? 1u : 0u) + name_len + 1u);
    if (!child) {
      uv_fs_req_cleanup(&req);
      return UV_ENOMEM;
    }
    
    memcpy(child, path, path_len);
    if (needs_sep) child[path_len++] = '/';
    memcpy(child + path_len, entry.name, name_len + 1u);
    
    result = fs_remove_path_recursive(child, true, force);
    free(child);
    if (result < 0) {
      uv_fs_req_cleanup(&req);
      return result;
    }
  }

  uv_fs_req_cleanup(&req);
  result = uv_fs_rmdir(NULL, &req, path, NULL);
  uv_fs_req_cleanup(&req);
  return (force && result == UV_ENOENT) ? 0 : result;
}

static bool fs_parse_rm_options(ant_t *js, ant_value_t options, bool *recursive_out, bool *force_out) {
  if (recursive_out) *recursive_out = false;
  if (force_out) *force_out = false;

  if (vtype(options) == T_UNDEF || vtype(options) == T_NULL) return true;
  if (vtype(options) != T_OBJ) return false;

  if (recursive_out) *recursive_out = js_truthy(js, js_get(js, options, "recursive"));
  if (force_out) *force_out = js_truthy(js, js_get(js, options, "force"));
  return true;
}

static ant_value_t fs_rm_impl(ant_t *js, ant_value_t *args, int nargs, bool return_promise) {
  ant_value_t promise = 0;
  ant_value_t path_val;
  size_t path_len = 0;
  const char *path = NULL;
  bool recursive = false;
  bool force = false;
  int result;

  if (return_promise) promise = js_mkpromise(js);
  if (nargs < 1) {
    ant_value_t err = js_mkerr(js, "rm() requires a path argument");
    if (!return_promise) return err;
    js_reject_promise(js, promise, err);
    return promise;
  }

  path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) {
    ant_value_t err = js_mkerr(js, "rm() path must be a string");
    if (!return_promise) return err;
    js_reject_promise(js, promise, err);
    return promise;
  }

  path = js_getstr(js, path_val, &path_len);
  if (!path) {
    ant_value_t err = js_mkerr(js, "Failed to get path string");
    if (!return_promise) return err;
    js_reject_promise(js, promise, err);
    return promise;
  }

  if (!fs_parse_rm_options(js, nargs >= 2 ? args[1] : js_mkundef(), &recursive, &force)) {
    ant_value_t err = js_mkerr_typed(js, JS_ERR_TYPE, "rm() options must be an object");
    if (!return_promise) return err;
    js_reject_promise(js, promise, err);
    return promise;
  }

  result = fs_remove_path_recursive(path, recursive, force);
  if (result < 0) {
    ant_value_t err = js_mkerr(js, "%s", uv_strerror(result));
    if (!return_promise) return err;
    js_reject_promise(js, promise, err);
    return promise;
  }

  if (!return_promise) return js_mkundef();
  js_resolve_promise(js, promise, js_mkundef());
  return promise;
}

static fs_encoding_t parse_encoding(ant_t *js, ant_value_t arg) {
  size_t len;
  const char *str = NULL;

  if (vtype(arg) == T_STR) str = js_getstr(js, arg, &len); 
  else if (vtype(arg) == T_OBJ) {
    ant_value_t enc = js_get(js, arg, "encoding");
    if (vtype(enc) == T_STR) str = js_getstr(js, enc, &len);
  }

  if (!str) return FS_ENC_NONE;

  if (len == 4 && memcmp(str, "utf8", 4) == 0)      return FS_ENC_UTF8;
  if (len == 5 && memcmp(str, "utf-8", 5) == 0)     return FS_ENC_UTF8;
  if (len == 7 && memcmp(str, "utf16le", 7) == 0)   return FS_ENC_UTF16LE;
  if (len == 4 && memcmp(str, "ucs2", 4) == 0)      return FS_ENC_UTF16LE;
  if (len == 5 && memcmp(str, "ucs-2", 5) == 0)     return FS_ENC_UTF16LE;
  if (len == 6 && memcmp(str, "latin1", 6) == 0)    return FS_ENC_LATIN1;
  if (len == 6 && memcmp(str, "binary", 6) == 0)    return FS_ENC_LATIN1;
  if (len == 6 && memcmp(str, "base64", 6) == 0)    return FS_ENC_BASE64;
  if (len == 9 && memcmp(str, "base64url", 9) == 0) return FS_ENC_BASE64URL;
  if (len == 3 && memcmp(str, "hex", 3) == 0)       return FS_ENC_HEX;
  if (len == 5 && memcmp(str, "ascii", 5) == 0)     return FS_ENC_ASCII;

  return FS_ENC_NONE;
}

static ant_value_t encode_data(ant_t *js, const char *data, size_t len, fs_encoding_t enc) {
  switch (enc) {
    case FS_ENC_UTF8:
    case FS_ENC_LATIN1:
    case FS_ENC_ASCII: return js_mkstr(js, data, len);

    case FS_ENC_HEX: {
      char *out = malloc(len * 2);
      if (!out) return js_mkerr(js, "Out of memory");
      for (size_t i = 0; i < len; i++) {
        out[i * 2] = hex_char((unsigned char)data[i] >> 4);
        out[i * 2 + 1] = hex_char(data[i]);
      }
      ant_value_t result = js_mkstr(js, out, len * 2);
      free(out); return result;
    }

    case FS_ENC_BASE64: {
      size_t out_len;
      char *out = ant_base64_encode((const uint8_t *)data, len, &out_len);
      if (!out) return js_mkerr(js, "Out of memory");
      ant_value_t result = js_mkstr(js, out, out_len);
      free(out); return result;
    }

    case FS_ENC_BASE64URL: {
      size_t out_len;
      char *out = ant_base64_encode((const uint8_t *)data, len, &out_len);
      if (!out) return js_mkerr(js, "Out of memory");
      for (size_t i = 0; i < out_len; i++) {
        if (out[i] == '+') out[i] = '-';
        else if (out[i] == '/') out[i] = '_';
      }
      while (out_len > 0 && out[out_len - 1] == '=') out_len--;
      ant_value_t result = js_mkstr(js, out, out_len);
      free(out); return result;
    }

    case FS_ENC_UTF16LE: {
      const unsigned char *s = (const unsigned char *)data;
      char *out = malloc((len / 2) * 4);
      if (!out) return js_mkerr(js, "Out of memory");
      size_t j = 0;
      for (size_t i = 0; i + 1 < len; i += 2) {
        uint32_t cp = s[i] | (s[i + 1] << 8);
        bool is_hi = cp >= 0xD800 && cp <= 0xDBFF && i + 3 < len;
        uint32_t lo = is_hi ? s[i + 2] | (s[i + 3] << 8) : 0;
        bool is_pair = is_hi && lo >= 0xDC00 && lo <= 0xDFFF;
        if (is_pair) { cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); i += 2; }
        j += utf8_encode(cp, out + j);
      }
      ant_value_t result = js_mkstr(js, out, j);
      free(out); return result;
    }

    default: return js_mkstr(js, data, len);
  }
}

static void free_fs_request(fs_request_t *req) {
  if (!req) return;

  if (req->path) free(req->path);
  if (req->path2) free(req->path2);
  if (req->data) free(req->data);
  if (req->error_msg) free(req->error_msg);
  
  uv_fs_req_cleanup(&req->uv_req);
  free(req);
}

static ant_value_t fs_rejected_promise(ant_t *js, ant_value_t err) {
  ant_value_t promise = js_mkpromise(js);
  js_reject_promise(js, promise, err);
  return promise;
}

static ant_value_t fs_resolved_promise(ant_t *js, ant_value_t value) {
  ant_value_t promise = js_mkpromise(js);
  js_resolve_promise(js, promise, value);
  return promise;
}

static ant_value_t fs_filehandle_require_this(ant_t *js) {
  ant_value_t this_obj = js_getthis(js);
  if (!is_object_type(this_obj) || !js_check_native_tag(this_obj, FS_FILEHANDLE_NATIVE_TAG))
    return js_mkerr_typed(js, JS_ERR_TYPE, "FileHandle method called on incompatible receiver");
  return this_obj;
}

static ant_value_t fs_filehandle_fd_value(ant_t *js, ant_value_t handle_obj) {
  ant_value_t fd_val = js_get_slot(handle_obj, SLOT_DATA);
  if (vtype(fd_val) != T_NUM) fd_val = js_get(js, handle_obj, "fd");
  return fd_val;
}

static ant_value_t fs_filehandle_get_fd(ant_t *js, ant_value_t handle_obj) {
  ant_value_t fd_val = fs_filehandle_fd_value(js, handle_obj);
  if (vtype(fd_val) != T_NUM || js_getnum(fd_val) < 0)
    return js_mkerr_typed(js, JS_ERR_TYPE, "FileHandle is closed");
  return fd_val;
}

static void fs_filehandle_mark_closed(ant_t *js, ant_value_t handle_obj) {
  js_set_slot(handle_obj, SLOT_DATA, js_mkundef());
  js_set(js, handle_obj, "fd", js_mknum(-1));
}

static ant_value_t builtin_fs_filehandle_close(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t handle_obj = fs_filehandle_require_this(js);
  if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj);

  ant_value_t fd_val = fs_filehandle_fd_value(js, handle_obj);
  if (vtype(fd_val) != T_NUM || js_getnum(fd_val) < 0)
    return fs_resolved_promise(js, js_mkundef());
    
  uv_fs_t req;
  int result = uv_fs_close(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), NULL);
  uv_fs_req_cleanup(&req);
  if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "close", NULL, NULL));

  fs_filehandle_mark_closed(js, handle_obj);
  return fs_resolved_promise(js, js_mkundef());
}

static ant_value_t builtin_fs_filehandle_stat(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t handle_obj = fs_filehandle_require_this(js);
  if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj);

  ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj);
  if (is_err(fd_val)) return fs_rejected_promise(js, fd_val);

  uv_fs_t req;
  int result = uv_fs_fstat(NULL, &req, (uv_file)(int)js_getnum(fd_val), NULL);
  if (result < 0) {
    ant_value_t err = fs_mk_uv_error(js, result, "fstat", NULL, NULL);
    uv_fs_req_cleanup(&req);
    return fs_rejected_promise(js, err);
  }

  ant_value_t stat_obj = fs_stats_object_from_uv(js, &req.statbuf);
  uv_fs_req_cleanup(&req);
  return fs_resolved_promise(js, stat_obj);
}

static ant_value_t builtin_fs_filehandle_sync(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t handle_obj = fs_filehandle_require_this(js);
  if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj);

  ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj);
  if (is_err(fd_val)) return fs_rejected_promise(js, fd_val);

  uv_fs_t req;
  int result = uv_fs_fsync(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), NULL);
  uv_fs_req_cleanup(&req);
  if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "fsync", NULL, NULL));

  return fs_resolved_promise(js, js_mkundef());
}

static ant_value_t builtin_fs_filehandle_read(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t handle_obj = fs_filehandle_require_this(js);
  if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj);

  ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj);
  if (is_err(fd_val)) return fs_rejected_promise(js, fd_val);
  if (nargs < 1) return fs_rejected_promise(js, js_mkerr(js, "FileHandle.read() requires a buffer argument"));

  TypedArrayData *ta_data = buffer_get_typedarray_data(args[0]);
  if (!ta_data || !ta_data->buffer || !ta_data->buffer->data)
    return fs_rejected_promise(js, js_mkerr(js, "FileHandle.read() buffer must be a Buffer or TypedArray"));

  size_t buf_len = ta_data->byte_length;
  size_t offset = 0;
  size_t length = buf_len;
  int64_t position = -1;

  if (nargs >= 2 && vtype(args[1]) == T_NUM) offset = (size_t)js_getnum(args[1]);
  if (nargs >= 3 && vtype(args[2]) == T_NUM) length = (size_t)js_getnum(args[2]);
  if (nargs >= 4 && vtype(args[3]) == T_NUM) position = (int64_t)js_getnum(args[3]);

  if (offset > buf_len) return fs_rejected_promise(js, js_mkerr(js, "offset is out of bounds"));
  if (offset + length > buf_len) return fs_rejected_promise(js, js_mkerr(js, "length extends beyond buffer"));

  uint8_t *buf_data = ta_data->buffer->data + ta_data->byte_offset;
  uv_buf_t buf = uv_buf_init((char *)(buf_data + offset), (unsigned int)length);
  
  uv_fs_t req;
  int result = uv_fs_read(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), &buf, 1, position, NULL);
  uv_fs_req_cleanup(&req);
  if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "read", NULL, NULL));

  ant_value_t out = js_mkobj(js);
  js_set(js, out, "bytesRead", js_mknum((double)result));
  js_set(js, out, "buffer", args[0]);
  
  return fs_resolved_promise(js, out);
}

static ant_value_t builtin_fs_filehandle_write(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t handle_obj = fs_filehandle_require_this(js);
  if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj);

  ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj);
  if (is_err(fd_val)) return fs_rejected_promise(js, fd_val);
  if (nargs < 1) return fs_rejected_promise(js, js_mkerr(js, "FileHandle.write() requires a data argument"));

  const char *write_data = NULL;
  size_t write_len = 0;
  int64_t position = -1;

  if (vtype(args[0]) == T_STR) {
    write_data = js_getstr(js, args[0], &write_len);
    if (!write_data) return fs_rejected_promise(js, js_mkerr(js, "Failed to get string"));
    if (nargs >= 2 && vtype(args[1]) == T_NUM) position = (int64_t)js_getnum(args[1]);
  } else {
    TypedArrayData *ta_data = buffer_get_typedarray_data(args[0]);
    if (!ta_data || !ta_data->buffer || !ta_data->buffer->data)
      return fs_rejected_promise(js, js_mkerr(js, "FileHandle.write() data must be a Buffer, TypedArray, DataView, or string"));
      
    uint8_t *buf_data = ta_data->buffer->data + ta_data->byte_offset;
    size_t buf_len = ta_data->byte_length;
    size_t offset = 0;
    size_t length = buf_len;

    if (nargs >= 2) {
    if (vtype(args[1]) == T_OBJ) {
      ant_value_t off_val = js_get(js, args[1], "offset");
      ant_value_t len_val = js_get(js, args[1], "length");
      ant_value_t pos_val = js_get(js, args[1], "position");
      if (vtype(off_val) == T_NUM) offset = (size_t)js_getnum(off_val);
      if (vtype(len_val) == T_NUM) length = (size_t)js_getnum(len_val);
      else length = buf_len - offset;
      if (vtype(pos_val) == T_NUM) position = (int64_t)js_getnum(pos_val);
    } else if (vtype(args[1]) == T_NUM) {
      offset = (size_t)js_getnum(args[1]);
      length = buf_len - offset;
      if (nargs >= 3 && vtype(args[2]) == T_NUM) length = (size_t)js_getnum(args[2]);
      if (nargs >= 4 && vtype(args[3]) == T_NUM) position = (int64_t)js_getnum(args[3]);
    }}

    if (offset > buf_len) return fs_rejected_promise(js, js_mkerr(js, "offset is out of bounds"));
    if (offset + length > buf_len) return fs_rejected_promise(js, js_mkerr(js, "length extends beyond buffer"));
    write_data = (const char *)(buf_data + offset);
    write_len = length;
  }

  uv_buf_t buf = uv_buf_init((char *)write_data, (unsigned int)write_len);
  uv_fs_t req;
  int result = uv_fs_write(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), &buf, 1, position, NULL);
  uv_fs_req_cleanup(&req);
  if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "write", NULL, NULL));

  ant_value_t out = js_mkobj(js);
  js_set(js, out, "bytesWritten", js_mknum((double)result));
  js_set(js, out, "buffer", args[0]);
  
  return fs_resolved_promise(js, out);
}

static ant_value_t builtin_fs_filehandle_writeFile(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t handle_obj = fs_filehandle_require_this(js);
  if (is_err(handle_obj)) return fs_rejected_promise(js, handle_obj);

  ant_value_t fd_val = fs_filehandle_get_fd(js, handle_obj);
  if (is_err(fd_val)) return fs_rejected_promise(js, fd_val);
  if (nargs < 1) return fs_rejected_promise(js, js_mkerr(js, "FileHandle.writeFile() requires data"));

  const char *data = NULL;
  size_t len = 0;

  if (vtype(args[0]) == T_STR) {
    data = js_getstr(js, args[0], &len);
    if (!data) return fs_rejected_promise(js, js_mkerr(js, "Failed to get string"));
  } else {
    TypedArrayData *ta_data = buffer_get_typedarray_data(args[0]);
    if (!ta_data || !ta_data->buffer || !ta_data->buffer->data)
      return fs_rejected_promise(js, js_mkerr(js, "FileHandle.writeFile() data must be a Buffer, TypedArray, DataView, or string"));
    data = (const char *)(ta_data->buffer->data + ta_data->byte_offset);
    len = ta_data->byte_length;
  }

  size_t written = 0;
  while (written < len) {
    uv_buf_t buf = uv_buf_init((char *)(data + written), (unsigned int)(len - written));
    uv_fs_t req;
    int result = uv_fs_write(uv_default_loop(), &req, (uv_file)(int)js_getnum(fd_val), &buf, 1, -1, NULL);
    uv_fs_req_cleanup(&req);
    if (result < 0) return fs_rejected_promise(js, fs_mk_uv_error(js, result, "write", NULL, NULL));
    if (result == 0) return fs_rejected_promise(js, js_mkerr(js, "short write"));
    written += (size_t)result;
  }

  return fs_resolved_promise(js, js_mkundef());
}

static void fs_init_filehandle_proto(ant_t *js) {
  if (is_object_type(g_filehandle_proto)) return;
  g_filehandle_proto = js_mkobj(js);
  js_set_native_tag(g_filehandle_proto, FS_FILEHANDLE_NATIVE_TAG);
  js_set(js, g_filehandle_proto, "close", js_mkfun(builtin_fs_filehandle_close));
  js_set(js, g_filehandle_proto, "stat", js_mkfun(builtin_fs_filehandle_stat));
  js_set(js, g_filehandle_proto, "sync", js_mkfun(builtin_fs_filehandle_sync));
  js_set(js, g_filehandle_proto, "read", js_mkfun(builtin_fs_filehandle_read));
  js_set(js, g_filehandle_proto, "write", js_mkfun(builtin_fs_filehandle_write));
  js_set(js, g_filehandle_proto, "writeFile", js_mkfun(builtin_fs_filehandle_writeFile));
  js_set_sym(js, g_filehandle_proto, get_toStringTag_sym(), js_mkstr(js, "FileHandle", 10));
}

static ant_value_t fs_make_filehandle(ant_t *js, int fd) {
  fs_init_filehandle_proto(js);
  ant_value_t handle = js_mkobj(js);
  
  js_set_native_tag(handle, FS_FILEHANDLE_NATIVE_TAG);
  js_set_proto_init(handle, g_filehandle_proto);
  js_set_slot(handle, SLOT_DATA, js_mknum((double)fd));
  js_set(js, handle, "fd", js_mknum((double)fd));
  
  return handle;
}

static void remove_pending_request(fs_request_t *req) {
  if (!req || !pending_requests) return;
  unsigned int len = utarray_len(pending_requests);
  
  for (unsigned int i = 0; i < len; i++) {
    if (*(fs_request_t**)utarray_eltptr(pending_requests, i) != req) continue;
    utarray_erase(pending_requests, i, 1); break;
  }
}

static ant_value_t fs_read_to_uint8array(ant_t *js, const char *data, size_t len) {
  ArrayBufferData *ab = create_array_buffer_data(len);
  if (!ab) return js_mkundef();
  memcpy(ab->data, data, len);
  ant_value_t result = create_typed_array(js, TYPED_ARRAY_UINT8, ab, 0, len, "Buffer");
  if (vtype(result) == T_ERR) free_array_buffer_data(ab);
  return result;
}

static void complete_request(fs_request_t *req) {
  if (req->failed) {
    const char *err_msg = req->error_msg 
      ? req->error_msg 
      : "Unknown error";
      
    ant_value_t props = js_mkobj(req->js);
    ant_value_t reject_value;

    if (req->error_code) {
      const char *code = uv_err_name(req->error_code);
      js_set(req->js, props, "code", js_mkstr(req->js, code, strlen(code)));
      js_set(req->js, props, "errno", js_mknum((double)req->error_code));
      if (req->path) js_set(req->js, props, "path", js_mkstr(req->js, req->path, strlen(req->path)));
      if (req->path2) js_set(req->js, props, "dest", js_mkstr(req->js, req->path2, strlen(req->path2)));
      reject_value = js_mkerr_props(req->js, JS_ERR_TYPE, props, "%s", err_msg);
    } else reject_value = js_mkerr(req->js, "%s", err_msg);

    if (is_err(reject_value)) {
      reject_value = req->js->thrown_exists ? req->js->thrown_value : js_mkundef();
      if (!req->js->thrown_exists)
        reject_value = js_mkstr(req->js, err_msg, strlen(err_msg));
      req->js->thrown_exists = false;
      req->js->thrown_value = js_mkundef();
    }
    js_reject_promise(req->js, req->promise, reject_value);
  } else {
    ant_value_t result = js_mkundef();
    if (req->op_type == FS_OP_READ && req->data) {
      if (req->encoding != FS_ENC_NONE)
        result = encode_data(req->js, req->data, req->data_len, req->encoding);
      else result = fs_read_to_uint8array(req->js, req->data, req->data_len);
    } else if (req->op_type == FS_OP_READ_BYTES && req->data)
      result = js_mkstr(req->js, req->data, req->data_len);
    else if (req->op_type == FS_OP_REALPATH && req->data)
      result = js_mkstr(req->js, req->data, req->data_len);
    else if (req->op_type == FS_OP_MKDTEMP && req->data)
      result = js_mkstr(req->js, req->data, req->data_len);
    js_resolve_promise(req->js, req->promise, result);
  }
  
  remove_pending_request(req);
  free_fs_request(req);
}

static void on_read_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  req->data_len = uv_req->result;
  uv_fs_t close_req;
  
  uv_fs_close(uv_default_loop(), &close_req, req->fd, NULL);
  uv_fs_req_cleanup(&close_req);
  
  req->completed = 1;
  complete_request(req);
}

static void on_open_for_read(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  req->fd = (int)uv_req->result;
  uv_fs_req_cleanup(uv_req);
  
  uv_fs_t stat_req;
  int stat_result = uv_fs_fstat(uv_default_loop(), &stat_req, req->fd, NULL);
  
  if (stat_result < 0) {
    fs_request_fail(req, stat_result);
    req->completed = 1;
    uv_fs_t close_req;
    uv_fs_close(uv_default_loop(), &close_req, req->fd, NULL);
    uv_fs_req_cleanup(&close_req);
    complete_request(req);
    return;
  }
  
  size_t file_size = stat_req.statbuf.st_size;
  uv_fs_req_cleanup(&stat_req);
  
  size_t alloc_size = (req->op_type == FS_OP_READ) ? file_size + 1 : file_size;
  req->data = malloc(alloc_size);
  if (!req->data) {
    req->failed = 1;
    req->error_msg = strdup("Out of memory");
    req->completed = 1;
    uv_fs_t close_req;
    uv_fs_close(uv_default_loop(), &close_req, req->fd, NULL);
    uv_fs_req_cleanup(&close_req);
    complete_request(req);
    return;
  }
  
  uv_buf_t buf = uv_buf_init(req->data, (unsigned int)file_size);
  int read_result = uv_fs_read(uv_default_loop(), uv_req, req->fd, &buf, 1, 0, on_read_complete);
  
  if (read_result < 0) {
    fs_request_fail(req, read_result);
    req->completed = 1;
    uv_fs_t close_req;
    uv_fs_close(uv_default_loop(), &close_req, req->fd, NULL);
    uv_fs_req_cleanup(&close_req);
    complete_request(req);
    return;
  }
}

static void on_write_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
  }
  
  uv_fs_t close_req;
  uv_fs_close(uv_default_loop(), &close_req, req->fd, NULL);
  uv_fs_req_cleanup(&close_req);
  
  req->completed = 1;
  complete_request(req);
}

static void on_open_for_write(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  req->fd = (int)uv_req->result;
  uv_fs_req_cleanup(uv_req);
  
  uv_buf_t buf = uv_buf_init(req->data, (unsigned int)req->data_len);
  int write_result = uv_fs_write(uv_default_loop(), uv_req, req->fd, &buf, 1, 0, on_write_complete);
  
  if (write_result < 0) {
    fs_request_fail(req, write_result);
    req->completed = 1;
    uv_fs_t close_req;
    uv_fs_close(uv_default_loop(), &close_req, req->fd, NULL);
    uv_fs_req_cleanup(&close_req);
    complete_request(req);
    return;
  }
}

static void on_unlink_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
  }
  
  uv_fs_req_cleanup(uv_req);
  req->completed = 1;
  complete_request(req);
}

static void on_rename_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;

  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
  }

  uv_fs_req_cleanup(uv_req);
  req->completed = 1;
  complete_request(req);
}

static int mkdirp(const char *path, mode_t mode) {
  int result = -1;
  char *tmp = strdup(path);
  if (!tmp) goto done;

  size_t len = strlen(tmp);
  if (len > 0 && tmp[len - 1] == '/') tmp[len - 1] = '\0';

  for (char *p = tmp + 1; *p; p++) {
    if (*p != '/') continue;
    *p = '\0';
#ifdef _WIN32
    _mkdir(tmp);
#else
    mkdir(tmp, mode);
#endif
    *p = '/';
  }

#ifdef _WIN32
  result = _mkdir(tmp);
#else
  result = mkdir(tmp, mode);
#endif
  if (result != 0 && errno == EEXIST) result = 0;

  free(tmp);
done:
  return result;
}

static void on_mkdir_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
  if (!req->recursive || uv_req->result != UV_EEXIST) {
    fs_request_fail(req, (int)uv_req->result);
  }}
  
  uv_fs_req_cleanup(uv_req);
  req->completed = 1;
  complete_request(req);
}

static void on_rmdir_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
  }
  
  uv_fs_req_cleanup(uv_req);
  req->completed = 1;
  complete_request(req);
}

static void on_stat_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  ant_value_t stat_obj = fs_stats_object_from_uv(req->js, &uv_req->statbuf);
  
  req->completed = 1;
  js_resolve_promise(req->js, req->promise, stat_obj);
  remove_pending_request(req);
  free_fs_request(req);
}

static void on_realpath_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;

  if (uv_req->result < 0 || !uv_req->ptr) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }

  req->data = strdup((const char *)uv_req->ptr);
  if (!req->data) {
    req->failed = 1;
    req->error_msg = strdup("Out of memory");
    req->completed = 1;
    complete_request(req);
    return;
  }

  req->data_len = strlen(req->data);
  req->completed = 1;
  complete_request(req);
}

static ant_value_t create_dirent_object(ant_t *js, const char *name, size_t name_len, uv_dirent_type_t type) {
  ant_value_t obj = js_newobj(js);
  js_set_proto(obj, g_dirent_proto);
  js_set(js, obj, "name", js_mkstr(js, name, name_len));
  js_set_slot(obj, SLOT_DATA, tov((double)type));
  return obj;
}

static void on_mkdtemp_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;

  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    uv_fs_req_cleanup(uv_req);
    return;
  }

  req->data = strdup(uv_req->path);
  if (!req->data) {
    req->failed = 1;
    req->error_msg = strdup("Out of memory");
    req->completed = 1;
    complete_request(req);
    uv_fs_req_cleanup(uv_req);
    return;
  }

  req->data_len = strlen(req->data);
  req->completed = 1;
  uv_fs_req_cleanup(uv_req);
  complete_request(req);
}

static void on_exists_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  ant_value_t result = js_bool(uv_req->result >= 0);
  
  req->completed = 1;
  js_resolve_promise(req->js, req->promise, result);
  remove_pending_request(req);
  free_fs_request(req);
}

static void on_access_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  req->completed = 1;
  js_resolve_promise(req->js, req->promise, js_mkundef());
  remove_pending_request(req);
  free_fs_request(req);
}

static void on_chmod_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;

  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }

  req->completed = 1;
  js_resolve_promise(req->js, req->promise, js_mkundef());
  remove_pending_request(req);
  free_fs_request(req);
}

static void on_readdir_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  ant_value_t arr = js_mkarr(req->js);
  uv_dirent_t dirent;
  
  while (uv_fs_scandir_next(uv_req, &dirent) != UV_EOF) {
  if (req->with_file_types) {
    ant_value_t entry = create_dirent_object(req->js, dirent.name, strlen(dirent.name), dirent.type);
    js_arr_push(req->js, arr, entry);
  } else {
    ant_value_t name = js_mkstr(req->js, dirent.name, strlen(dirent.name));
    js_arr_push(req->js, arr, name);
  }}
  
  req->completed = 1;
  js_resolve_promise(req->js, req->promise, arr);
  remove_pending_request(req);
  free_fs_request(req);
}

static ant_value_t builtin_fs_readFileSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readFileSync() requires a path argument");
  
  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "readFileSync() path must be a string or URL");
  
  size_t path_len;
  char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  FILE *file = fopen(path_cstr, "rb");
  if (!file) {
    ant_value_t err = fs_mk_errno_error(js, errno, "open", path_cstr, NULL);
    free(path_cstr);
    return err;
  }
  
  fseek(file, 0, SEEK_END);
  long file_size = ftell(file);
  fseek(file, 0, SEEK_SET);
  
  if (file_size < 0) {
    fclose(file);
    free(path_cstr);
    return js_mkerr(js, "Failed to get file size");
  }
  
  char *data = malloc(file_size + 1);
  if (!data) {
    fclose(file);
    free(path_cstr);
    return js_mkerr(js, "Out of memory");
  }
  
  size_t bytes_read = fread(data, 1, file_size, file);
  fclose(file);
  free(path_cstr);
  
  if (bytes_read != (size_t)file_size) {
    free(data);
    return js_mkerr(js, "Failed to read entire file");
  }
  
  fs_encoding_t enc = (nargs > 1) ? parse_encoding(js, args[1]) : FS_ENC_NONE;
  ant_value_t result = (enc != FS_ENC_NONE)
    ? encode_data(js, data, file_size, enc)
    : fs_read_to_uint8array(js, data, file_size);
    
  free(data);
  return result;
}

static ant_value_t builtin_fs_readBytesSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readBytesSync() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "readBytesSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  FILE *file = fopen(path_cstr, "rb");
  if (!file) {
    ant_value_t err = fs_mk_errno_error(js, errno, "open", path_cstr, NULL);
    free(path_cstr);
    return err;
  }
  
  fseek(file, 0, SEEK_END);
  long file_size = ftell(file);
  fseek(file, 0, SEEK_SET);
  
  if (file_size < 0) {
    fclose(file);
    free(path_cstr);
    return js_mkerr(js, "Failed to get file size");
  }
  
  char *data = malloc(file_size);
  if (!data) {
    fclose(file);
    free(path_cstr);
    return js_mkerr(js, "Out of memory");
  }
  
  size_t bytes_read = fread(data, 1, file_size, file);
  fclose(file);
  free(path_cstr);
  
  if (bytes_read != (size_t)file_size) {
    free(data);
    return js_mkerr(js, "Failed to read entire file");
  }
  
  ant_value_t result = js_mkstr(js, data, file_size);
  free(data);
  
  return result;
}

static ant_value_t builtin_fs_readFile(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readFile() requires a path argument");
  
  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "readFile() path must be a string or URL");
  
  size_t path_len;
  char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_READ;
  req->encoding = (nargs > 1) ? parse_encoding(js, args[1]) : FS_ENC_NONE;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_open(uv_default_loop(), &req->uv_req, req->path, O_RDONLY, 0, on_open_for_read);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_readBytes(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readBytes() requires a path argument");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "readBytes() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_READ_BYTES;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_open(uv_default_loop(), &req->uv_req, req->path, O_RDONLY, 0, on_open_for_read);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t fs_write_file_sync_impl(
  ant_t *js,
  ant_value_t *args,
  int nargs,
  const char *fn_name,
  const char *mode
) {
  if (nargs < 2) return js_mkerr(js, "%s() requires path and data arguments", fn_name);
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "%s() path must be a string", fn_name);
  if (vtype(args[1]) != T_STR) return js_mkerr(js, "%s() data must be a string", fn_name);

  size_t path_len, data_len;
  char *path = js_getstr(js, args[0], &path_len);
  char *data = js_getstr(js, args[1], &data_len);
  if (!path || !data) return js_mkerr(js, "Failed to get arguments");

  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");

  FILE *file = fopen(path_cstr, mode);
  if (!file) {
    ant_value_t err = fs_mk_errno_error(js, errno, "open", path_cstr, NULL);
    free(path_cstr);
    return err;
  }

  size_t bytes_written = fwrite(data, 1, data_len, file);
  fclose(file);
  free(path_cstr);

  if (bytes_written != data_len) {
    return js_mkerr(js, "Failed to write entire file");
  }

  return js_mkundef();
}

static ant_value_t builtin_fs_writeFileSync(ant_t *js, ant_value_t *args, int nargs) {
  return fs_write_file_sync_impl(js, args, nargs, "writeFileSync", "wb");
}

static ant_value_t builtin_fs_copyFileSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "copyFileSync() requires src and dest arguments");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "copyFileSync() src must be a string");
  if (vtype(args[1]) != T_STR) return js_mkerr(js, "copyFileSync() dest must be a string");
  
  size_t src_len, dest_len;
  char *src = js_getstr(js, args[0], &src_len);
  char *dest = js_getstr(js, args[1], &dest_len);
  
  if (!src || !dest) return js_mkerr(js, "Failed to get arguments");
  
  char *src_cstr = strndup(src, src_len);
  char *dest_cstr = strndup(dest, dest_len);
  if (!src_cstr || !dest_cstr) {
    free(src_cstr);
    free(dest_cstr);
    return js_mkerr(js, "Out of memory");
  }
  
  FILE *in = fopen(src_cstr, "rb");
  if (!in) {
    ant_value_t err = fs_mk_errno_error(js, errno, "copyfile", src_cstr, dest_cstr);
    free(src_cstr); free(dest_cstr);
    return err;  
  }
  
  FILE *out = fopen(dest_cstr, "wb");
  if (!out) {
    ant_value_t err = fs_mk_errno_error(js, errno, "copyfile", src_cstr, dest_cstr);
    fclose(in);
    free(src_cstr); free(dest_cstr);
    return err;
  }
  
  char buf[8192];
  size_t n;
  
  while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
  if (fwrite(buf, 1, n, out) != n) {
    ant_value_t err = fs_mk_errno_error(js, errno ? errno : EIO, "copyfile", src_cstr, dest_cstr);
    fclose(in); fclose(out);
    free(src_cstr); free(dest_cstr);
    return err;
  }}
  
  fclose(in); fclose(out);
  free(src_cstr); free(dest_cstr);

  return js_mkundef();
}

typedef struct {
  bool recursive;
  bool force;
  bool error_on_exist;
} fs_cp_options_t;

static void fs_parse_cp_options(ant_t *js, ant_value_t value, fs_cp_options_t *opts) {
  opts->recursive = false;
  opts->force = true;
  opts->error_on_exist = false;

  if (vtype(value) != T_OBJ) return;

  ant_value_t recursive = js_get(js, value, "recursive");
  ant_value_t force = js_get(js, value, "force");
  ant_value_t error_on_exist = js_get(js, value, "errorOnExist");

  if (!is_undefined(recursive)) opts->recursive = js_truthy(js, recursive);
  if (!is_undefined(force)) opts->force = js_truthy(js, force);
  if (!is_undefined(error_on_exist)) opts->error_on_exist = js_truthy(js, error_on_exist);
}

static char *fs_join_path(const char *base, const char *name) {
  size_t base_len = strlen(base);
  size_t name_len = strlen(name);
  bool need_sep = base_len > 0 && base[base_len - 1] != '/';
  size_t total_len = base_len + (need_sep ? 1 : 0) + name_len;

  char *joined = calloc(total_len + 1, 1);
  if (!joined) return NULL;

  memcpy(joined, base, base_len);
  if (need_sep) joined[base_len++] = '/';
  memcpy(joined + base_len, name, name_len);
  joined[total_len] = '\0';
  return joined;
}

static ant_value_t fs_copy_file_sync_impl(
  ant_t *js,
  const char *src_cstr,
  const char *dest_cstr,
  const fs_cp_options_t *opts,
  const char *op_name
) {
  struct stat dest_st;
  if (!opts->force && stat(dest_cstr, &dest_st) == 0) {
    if (opts->error_on_exist) return fs_mk_errno_error(js, EEXIST, op_name, src_cstr, dest_cstr);
    return js_mkundef();
  }

  FILE *in = fopen(src_cstr, "rb");
  if (!in) return fs_mk_errno_error(js, errno, op_name, src_cstr, dest_cstr);

  FILE *out = fopen(dest_cstr, "wb");
  if (!out) {
    ant_value_t err = fs_mk_errno_error(js, errno, op_name, src_cstr, dest_cstr);
    fclose(in);
    return err;
  }

  char buf[8192];
  size_t n = 0;
  while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
  if (fwrite(buf, 1, n, out) != n) {
    ant_value_t err = fs_mk_errno_error(js, errno ? errno : EIO, op_name, src_cstr, dest_cstr);
    fclose(in);
    fclose(out);
    return err;
  }}

  fclose(in);
  fclose(out);
  
  return js_mkundef();
}

static ant_value_t builtin_fs_copyFile(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "copyFile() requires src and dest arguments");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "copyFile() src must be a string");
  if (vtype(args[1]) != T_STR) return js_mkerr(js, "copyFile() dest must be a string");

  size_t src_len = 0, dest_len = 0;
  const char *src = js_getstr(js, args[0], &src_len);
  const char *dest = js_getstr(js, args[1], &dest_len);
  if (!src || !dest) return js_mkerr(js, "Failed to get arguments");

  char *src_cstr = strndup(src, src_len);
  char *dest_cstr = strndup(dest, dest_len);
  if (!src_cstr || !dest_cstr) {
    free(src_cstr);
    free(dest_cstr);
    return js_mkerr(js, "Out of memory");
  }

  fs_cp_options_t opts = {
    .recursive = false,
    .force = true,
    .error_on_exist = false,
  };

  ant_value_t promise = js_mkpromise(js);
  ant_value_t result = fs_copy_file_sync_impl(js, src_cstr, dest_cstr, &opts, "copyFile");
  free(src_cstr);
  free(dest_cstr);

  if (is_err(result)) js_reject_promise(js, promise, result);
  else js_resolve_promise(js, promise, js_mkundef());
  
  return promise;
}

static ant_value_t fs_copy_path_sync_impl(
  ant_t *js,
  const char *src_cstr,
  const char *dest_cstr,
  const fs_cp_options_t *opts,
  const char *op_name
) {
  struct stat src_st;
  if (stat(src_cstr, &src_st) != 0) return fs_mk_errno_error(js, errno, op_name, src_cstr, dest_cstr);

  if ((src_st.st_mode & S_IFMT) == S_IFDIR) {
    if (!opts->recursive) {
      return js_mkerr(js, "%s() recursive option is required to copy directories", op_name);
    }

    struct stat dest_st;
    if (stat(dest_cstr, &dest_st) != 0) {
      if (errno != ENOENT) return fs_mk_errno_error(js, errno, op_name, src_cstr, dest_cstr);
    #ifdef _WIN32
      if (_mkdir(dest_cstr) != 0) return fs_mk_errno_error(js, errno, op_name, src_cstr, dest_cstr);
    #else
      if (mkdir(dest_cstr, (mode_t)(src_st.st_mode & 0777)) != 0) {
        return fs_mk_errno_error(js, errno, op_name, src_cstr, dest_cstr);
      }
    #endif
    } else if ((dest_st.st_mode & S_IFMT) != S_IFDIR) {
      return fs_mk_errno_error(js, EEXIST, op_name, src_cstr, dest_cstr);
    }

    uv_fs_t req;
    int rc = uv_fs_scandir(NULL, &req, src_cstr, 0, NULL);
    if (rc < 0) {
      ant_value_t err = fs_mk_uv_error(js, rc, "scandir", src_cstr, dest_cstr);
      uv_fs_req_cleanup(&req);
      return err;
    }

    uv_dirent_t dirent;
    ant_value_t result = js_mkundef();
    while (uv_fs_scandir_next(&req, &dirent) != UV_EOF) {
      if (
        strcmp(dirent.name, ".") == 0 ||
        strcmp(dirent.name, "..") == 0
      ) continue;
      
      char *child_src = fs_join_path(src_cstr, dirent.name);
      char *child_dest = fs_join_path(dest_cstr, dirent.name);
      if (!child_src || !child_dest) {
        free(child_src);
        free(child_dest);
        result = js_mkerr(js, "Out of memory");
        break;
      }
      
      result = fs_copy_path_sync_impl(js, child_src, child_dest, opts, op_name);
      free(child_src);
      free(child_dest);
      if (is_err(result)) break;
    }
    
    uv_fs_req_cleanup(&req);
    return result;
  }

  return fs_copy_file_sync_impl(js, src_cstr, dest_cstr, opts, op_name);
}

static ant_value_t fs_cp_sync_common(
  ant_t *js,
  ant_value_t *args,
  int nargs,
  const char *fn_name
) {
  if (nargs < 2) return js_mkerr(js, "%s() requires src and dest arguments", fn_name);
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "%s() src must be a string", fn_name);
  if (vtype(args[1]) != T_STR) return js_mkerr(js, "%s() dest must be a string", fn_name);

  size_t src_len = 0, dest_len = 0;
  const char *src = js_getstr(js, args[0], &src_len);
  const char *dest = js_getstr(js, args[1], &dest_len);
  if (!src || !dest) return js_mkerr(js, "Failed to get arguments");

  fs_cp_options_t opts;
  fs_parse_cp_options(js, nargs >= 3 ? args[2] : js_mkundef(), &opts);

  char *src_cstr = strndup(src, src_len);
  char *dest_cstr = strndup(dest, dest_len);
  if (!src_cstr || !dest_cstr) {
    free(src_cstr);
    free(dest_cstr);
    return js_mkerr(js, "Out of memory");
  }

  ant_value_t result = fs_copy_path_sync_impl(js, src_cstr, dest_cstr, &opts, fn_name);
  free(src_cstr);
  free(dest_cstr);
  return result;
}

static ant_value_t builtin_fs_cpSync(ant_t *js, ant_value_t *args, int nargs) {
  return fs_cp_sync_common(js, args, nargs, "cpSync");
}

static ant_value_t builtin_fs_cp(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t promise = js_mkpromise(js);
  ant_value_t result = fs_cp_sync_common(js, args, nargs, "cp");
  if (is_err(result)) js_reject_promise(js, promise, result);
  else js_resolve_promise(js, promise, js_mkundef());
  return promise;
}

static ant_value_t builtin_fs_renameSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "renameSync() requires oldPath and newPath arguments");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "renameSync() oldPath must be a string");
  if (vtype(args[1]) != T_STR) return js_mkerr(js, "renameSync() newPath must be a string");
  
  size_t old_len, new_len;
  char *old_path = js_getstr(js, args[0], &old_len);
  char *new_path = js_getstr(js, args[1], &new_len);
  
  if (!old_path || !new_path) return js_mkerr(js, "Failed to get arguments");
  
  char *old_cstr = strndup(old_path, old_len);
  char *new_cstr = strndup(new_path, new_len);
  if (!old_cstr || !new_cstr) {
    free(old_cstr);
    free(new_cstr);
    return js_mkerr(js, "Out of memory");
  }
  
  int result = rename(old_cstr, new_cstr);
  
  if (result != 0) {
    ant_value_t err = fs_mk_errno_error(js, errno, "rename", old_cstr, new_cstr);
    free(old_cstr);
    free(new_cstr);
    return err;
  }
  
  free(old_cstr);
  free(new_cstr);
  
  return js_mkundef();
}

static ant_value_t builtin_fs_rename(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "rename() requires oldPath and newPath arguments");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "rename() oldPath must be a string");
  if (vtype(args[1]) != T_STR) return js_mkerr(js, "rename() newPath must be a string");

  size_t old_len = 0, new_len = 0;
  const char *old_path = js_getstr(js, args[0], &old_len);
  const char *new_path = js_getstr(js, args[1], &new_len);
  if (!old_path || !new_path) return js_mkerr(js, "Failed to get arguments");

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_RENAME;
  req->promise = js_mkpromise(js);
  req->path = strndup(old_path, old_len);
  req->path2 = strndup(new_path, new_len);
  req->uv_req.data = req;

  if (!req->path || !req->path2) {
    free_fs_request(req);
    return js_mkerr(js, "Out of memory");
  }

  utarray_push_back(pending_requests, &req);
  int result = uv_fs_rename(uv_default_loop(), &req->uv_req, req->path, req->path2, on_rename_complete);

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static double fs_time_arg_to_seconds(ant_t *js, ant_value_t v) {
  if (is_date_instance(v)) {
    ant_value_t t = js_get_slot(js_as_obj(v), SLOT_DATA);
    return (vtype(t) == T_NUM) ? js_getnum(t) / 1000.0 : 0.0;
  }
  return js_to_number(js, v);
}

static ant_value_t builtin_fs_utimesSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 3) return js_mkerr(js, "utimesSync() requires path, atime, and mtime");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "utimesSync() path must be a string");

  const char *path = js_str(js, args[0]);
  double atime = fs_time_arg_to_seconds(js, args[1]);
  double mtime = fs_time_arg_to_seconds(js, args[2]);

  uv_fs_t req;
  int rc = uv_fs_utime(NULL, &req, path, atime, mtime, NULL);
  uv_fs_req_cleanup(&req);

  if (rc < 0) return js_mkerr(js, "utimesSync: %s", uv_strerror(rc));
  return js_mkundef();
}

static ant_value_t builtin_fs_utimes(ant_t *js, ant_value_t *args, int nargs) {
  return builtin_fs_utimesSync(js, args, nargs);
}

static ant_value_t builtin_fs_futimesSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 3) return js_mkerr(js, "futimesSync() requires fd, atime, and mtime");
  int fd = (int)js_to_number(js, args[0]);
  double atime = fs_time_arg_to_seconds(js, args[1]);
  double mtime = fs_time_arg_to_seconds(js, args[2]);

  uv_fs_t req;
  int rc = uv_fs_futime(NULL, &req, fd, atime, mtime, NULL);
  uv_fs_req_cleanup(&req);

  if (rc < 0) return js_mkerr(js, "futimesSync: %s", uv_strerror(rc));
  return js_mkundef();
}

static ant_value_t builtin_fs_futimes(ant_t *js, ant_value_t *args, int nargs) {
  return builtin_fs_futimesSync(js, args, nargs);
}

static ant_value_t builtin_fs_appendFileSync(ant_t *js, ant_value_t *args, int nargs) {
  return fs_write_file_sync_impl(js, args, nargs, "appendFileSync", "ab");
}

static ant_value_t builtin_fs_appendFile(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t promise = js_mkpromise(js);
  ant_value_t result = fs_write_file_sync_impl(js, args, nargs, "appendFile", "ab");
  if (is_err(result)) js_reject_promise(js, promise, result);
  else js_resolve_promise(js, promise, js_mkundef());
  return promise;
}

static ant_value_t builtin_fs_writeFile(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "writeFile() requires path and data arguments");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "writeFile() path must be a string");
  if (vtype(args[1]) != T_STR) return js_mkerr(js, "writeFile() data must be a string");
  
  size_t path_len, data_len;
  char *path = js_getstr(js, args[0], &path_len);
  char *data = js_getstr(js, args[1], &data_len);
  
  if (!path || !data) return js_mkerr(js, "Failed to get arguments");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_WRITE;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->data = malloc(data_len);
  if (!req->data) {
    free(req->path);
    free(req);
    return js_mkerr(js, "Out of memory");
  }
  
  memcpy(req->data, data, data_len);
  req->data_len = data_len;
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_open(uv_default_loop(), &req->uv_req, req->path, O_WRONLY | O_CREAT | O_TRUNC, 0644, on_open_for_write);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_unlinkSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "unlinkSync() requires a path argument");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "unlinkSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  int result = unlink(path_cstr);
  
  if (result != 0) {
    ant_value_t err = fs_mk_errno_error(js, errno, "unlink", path_cstr, NULL);
    free(path_cstr);
    return err;
  }
  
  free(path_cstr);
  return js_mkundef();
}

static ant_value_t builtin_fs_unlink(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "unlink() requires a path argument");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "unlink() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_UNLINK;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_unlink(uv_default_loop(), &req->uv_req, req->path, on_unlink_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_mkdirSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "mkdirSync() requires a path argument");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "mkdirSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  int mode = 0755;
  int recursive = 0;
  
  if (nargs < 2) goto do_mkdir;
  
  switch (vtype(args[1])) {
    case T_NUM:
      mode = (int)js_getnum(args[1]);
      break;
    case T_OBJ: {
      ant_value_t opt = args[1];
      recursive = js_get(js, opt, "recursive") == js_true;
      ant_value_t mode_val = js_get(js, opt, "mode");
      if (vtype(mode_val) == T_NUM) mode = (int)js_getnum(mode_val);
      break;
    }
  }
  
do_mkdir:
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  int result;
  if (recursive) {
    result = mkdirp(path_cstr, (mode_t)mode);
  } else {
#ifdef _WIN32
    (void)mode;
    result = _mkdir(path_cstr);
#else
    result = mkdir(path_cstr, (mode_t)mode);
#endif
  }
  if (result != 0) {
    ant_value_t err = fs_mk_errno_error(js, errno, "mkdir", path_cstr, NULL);
    free(path_cstr);
    return err;
  }
  
  free(path_cstr);
  
  return js_mkundef();
}

static ant_value_t builtin_fs_mkdir(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "mkdir() requires a path argument");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "mkdir() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  int mode = 0755;
  int recursive = 0;
  if (nargs >= 2) {
  switch (vtype(args[1])) {
    case T_NUM:
      mode = (int)js_getnum(args[1]);
      break;
    case T_OBJ: {
      ant_value_t opt = args[1];
      recursive = js_get(js, opt, "recursive") == js_true;
      ant_value_t mode_val = js_get(js, opt, "mode");
      if (vtype(mode_val) == T_NUM) mode = (int)js_getnum(mode_val);
      break;
    }}
  }

  if (recursive) {
    ant_value_t promise = js_mkpromise(js);
    char *path_cstr = strndup(path, path_len);
    
    if (!path_cstr) return js_mkerr(js, "Out of memory");
    int result = mkdirp(path_cstr, (mode_t)mode);
    
    free(path_cstr);
    if (result != 0) {
      js_reject_promise(js, promise, js_mkerr(js, "Failed to create directory: %s", strerror(errno)));
    } else js_resolve_promise(js, promise, js_mkundef());
    
    return promise;
  }

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_MKDIR;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->recursive = recursive;
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_mkdir(uv_default_loop(), &req->uv_req, req->path, mode, on_mkdir_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_mkdtempSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1 || vtype(args[0]) != T_STR)
    return js_mkerr(js, "mkdtempSync() requires a prefix string");

  size_t prefix_len;
  const char *prefix = js_getstr(js, args[0], &prefix_len);
  if (!prefix) return js_mkerr(js, "Failed to get prefix string");

  size_t tpl_len = prefix_len + 6;
  char *tpl = malloc(tpl_len + 1);
  if (!tpl) return js_mkerr(js, "Out of memory");

  memcpy(tpl, prefix, prefix_len);
  memcpy(tpl + prefix_len, "XXXXXX", 6);
  tpl[tpl_len] = '\0';

  char *result = mkdtemp(tpl);
  if (!result) {
    free(tpl);
    return js_mkerr(js, "mkdtempSync failed: %s", strerror(errno));
  }

  ant_value_t ret = js_mkstr(js, result, strlen(result));
  free(tpl);
  return ret;
}

static ant_value_t builtin_fs_mkdtemp(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1 || vtype(args[0]) != T_STR)
    return js_mkerr(js, "mkdtemp() requires a prefix string");

  size_t prefix_len;
  const char *prefix = js_getstr(js, args[0], &prefix_len);
  if (!prefix) return js_mkerr(js, "Failed to get prefix string");

  size_t tpl_len = prefix_len + 6;
  char *tpl = malloc(tpl_len + 1);
  if (!tpl) return js_mkerr(js, "Out of memory");

  memcpy(tpl, prefix, prefix_len);
  memcpy(tpl + prefix_len, "XXXXXX", 6);
  tpl[tpl_len] = '\0';

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) { free(tpl); return js_mkerr(js, "Out of memory"); }

  req->js = js;
  req->op_type = FS_OP_MKDTEMP;
  req->promise = js_mkpromise(js);
  req->path = tpl;
  req->uv_req.data = req;

  utarray_push_back(pending_requests, &req);
  int result = uv_fs_mkdtemp(uv_default_loop(), &req->uv_req, req->path, on_mkdtemp_complete);

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static ant_value_t builtin_fs_rmdirSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "rmdirSync() requires a path argument");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "rmdirSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
#ifdef _WIN32
  int result = _rmdir(path_cstr);
#else
  int result = rmdir(path_cstr);
#endif
  if (result != 0) {
    ant_value_t err = fs_mk_errno_error(js, errno, "rmdir", path_cstr, NULL);
    free(path_cstr);
    return err;
  }
  
  free(path_cstr);
  return js_mkundef();
}

static ant_value_t builtin_fs_rmdir(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "rmdir() requires a path argument");
  
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "rmdir() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_RMDIR;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_rmdir(uv_default_loop(), &req->uv_req, req->path, on_rmdir_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_rmSync(ant_t *js, ant_value_t *args, int nargs) {
  return fs_rm_impl(js, args, nargs, false);
}

static ant_value_t builtin_fs_rm(ant_t *js, ant_value_t *args, int nargs) {
  return fs_rm_impl(js, args, nargs, true);
}

static ant_value_t dirent_isFile(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t type_val = js_get_slot(this, SLOT_DATA);
  if (vtype(type_val) != T_NUM) return js_false;
  return js_bool((int)js_getnum(type_val) == UV_DIRENT_FILE);
}

static ant_value_t dirent_isDirectory(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t type_val = js_get_slot(this, SLOT_DATA);
  if (vtype(type_val) != T_NUM) return js_false;
  return js_bool((int)js_getnum(type_val) == UV_DIRENT_DIR);
}

static ant_value_t dirent_isSymbolicLink(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t type_val = js_get_slot(this, SLOT_DATA);
  if (vtype(type_val) != T_NUM) return js_false;
  return js_bool((int)js_getnum(type_val) == UV_DIRENT_LINK);
}

static ant_value_t dirent_isBlockDevice(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t type_val = js_get_slot(this, SLOT_DATA);
  if (vtype(type_val) != T_NUM) return js_false;
  return js_bool((int)js_getnum(type_val) == UV_DIRENT_BLOCK);
}

static ant_value_t dirent_isCharacterDevice(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t type_val = js_get_slot(this, SLOT_DATA);
  if (vtype(type_val) != T_NUM) return js_false;
  return js_bool((int)js_getnum(type_val) == UV_DIRENT_CHAR);
}

static ant_value_t dirent_isFIFO(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t type_val = js_get_slot(this, SLOT_DATA);
  if (vtype(type_val) != T_NUM) return js_false;
  return js_bool((int)js_getnum(type_val) == UV_DIRENT_FIFO);
}

static ant_value_t dirent_isSocket(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t type_val = js_get_slot(this, SLOT_DATA);
  if (vtype(type_val) != T_NUM) return js_false;
  return js_bool((int)js_getnum(type_val) == UV_DIRENT_SOCKET);
}

static ant_value_t stat_isFile(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t mode_val = js_get_slot(this, SLOT_DATA);
  
  if (vtype(mode_val) != T_NUM) return js_false;
  mode_t mode = (mode_t)js_getnum(mode_val);
  
  return js_bool(S_ISREG(mode));
}

static ant_value_t stat_isDirectory(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t mode_val = js_get_slot(this, SLOT_DATA);
  
  if (vtype(mode_val) != T_NUM) return js_false;
  mode_t mode = (mode_t)js_getnum(mode_val);
  
  return js_bool(S_ISDIR(mode));
}

static ant_value_t stat_isSymbolicLink(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t this = js_getthis(js);
  ant_value_t mode_val = js_get_slot(this, SLOT_DATA);
  
  if (vtype(mode_val) != T_NUM) return js_false;
  mode_t mode = (mode_t)js_getnum(mode_val);
  
  return js_bool(S_ISLNK(mode));
}

static ant_value_t create_stats_object(ant_t *js, struct stat *st) {
  return fs_stats_object_from_posix(js, st);
}

static const char *errno_to_code(int err_num) {
switch (err_num) {
  case ENOENT: return "ENOENT";
  case EACCES: return "EACCES";
  case ENOTDIR: return "ENOTDIR";
  case ELOOP: return "ELOOP";
  case ENAMETOOLONG: return "ENAMETOOLONG";
  case EOVERFLOW: return "EOVERFLOW";
  case EROFS: return "EROFS";
  case ETXTBSY: return "ETXTBSY";
  case EEXIST: return "EEXIST";
  case ENOTEMPTY: return "ENOTEMPTY";
  case EISDIR: return "EISDIR";
  case EBUSY: return "EBUSY";
  case EINVAL: return "EINVAL";
  case EPERM: return "EPERM";
  case EIO: return "EIO";
  default: return "UNKNOWN";
}}

static ant_value_t fs_mk_syscall_error(
  ant_t *js, const char *code, double err_num,
  const char *message, const char *syscall,
  const char *path, const char *dest
) {
  ant_value_t props = js_mkobj(js);

  if (!code) code = "UNKNOWN";
  if (!message) message = "Unknown error";

  js_set(js, props, "code", js_mkstr(js, code, strlen(code)));
  js_set(js, props, "errno", js_mknum(err_num));
  
  if (syscall) js_set(js, props, "syscall", js_mkstr(js, syscall, strlen(syscall)));
  if (path) js_set(js, props, "path", js_mkstr(js, path, strlen(path)));
  if (dest) js_set(js, props, "dest", js_mkstr(js, dest, strlen(dest)));

  if (path && dest) return js_mkerr_props(
    js, JS_ERR_GENERIC,
    props, "%s: %s, %s '%s' -> '%s'",
    code, message, syscall, path, dest
  );
    
  if (path) return js_mkerr_props(
    js, JS_ERR_GENERIC,
    props, "%s: %s, %s '%s'",
    code, message, syscall, path
  );
  
  return js_mkerr_props(
    js, JS_ERR_GENERIC, 
    props, "%s: %s, %s",
    code, message, syscall
  );
}

static ant_value_t fs_mk_errno_error(
  ant_t *js, int err_num,
  const char *syscall, const char *path, const char *dest
) {
  int uv_code = uv_translate_sys_error(err_num);
  if (uv_code == 0) uv_code = -err_num;
  
  return fs_mk_syscall_error(
    js, errno_to_code(err_num),
    (double)uv_code,
    uv_strerror(uv_code),
    syscall, path, dest
  );
}

static ant_value_t fs_mk_uv_error(
  ant_t *js, int uv_code,
  const char *syscall, const char *path, const char *dest
) {
  return fs_mk_syscall_error(
    js, uv_err_name(uv_code),
    (double)uv_code,
    uv_strerror(uv_code),
    syscall, path, dest
  );
}

static ant_value_t builtin_fs_statSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "statSync() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "statSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  struct stat st;
  int result = stat(path_cstr, &st);
  
  if (result != 0) {
    ant_value_t err = fs_mk_errno_error(js, errno, "stat", path_cstr, NULL);
    free(path_cstr); return err;
  }
  
  free(path_cstr);
  return create_stats_object(js, &st);
}

static ant_value_t builtin_fs_stat(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "stat() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "stat() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_STAT;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_stat(uv_default_loop(), &req->uv_req, req->path, on_stat_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_lstatSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "lstatSync() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "lstatSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  uv_fs_t req;
  int result = uv_fs_lstat(NULL, &req, path_cstr, NULL);

  if (result < 0) {
    ant_value_t err = fs_mk_uv_error(js, result, "lstat", path_cstr, NULL);
    uv_fs_req_cleanup(&req);
    free(path_cstr);
    return err;
  }

  ant_value_t stat_obj = fs_stats_object_from_uv(js, &req.statbuf);
  uv_fs_req_cleanup(&req);
  free(path_cstr);
  
  return stat_obj;
}

static ant_value_t builtin_fs_lstat(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "lstat() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "lstat() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_STAT;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_lstat(uv_default_loop(), &req->uv_req, req->path, on_stat_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_fstatSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "fstatSync() requires an fd argument");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "fstatSync() fd must be a number");

  uv_file fd = (uv_file)(int)js_getnum(args[0]);
  uv_fs_t req;
  int result = uv_fs_fstat(NULL, &req, fd, NULL);

  if (result < 0) {
    ant_value_t err = fs_mk_uv_error(js, result, "fstat", NULL, NULL);
    uv_fs_req_cleanup(&req);
    return err;
  }

  ant_value_t stat_obj = fs_stats_object_from_uv(js, &req.statbuf);
  uv_fs_req_cleanup(&req);
  return stat_obj;
}

static ant_value_t builtin_fs_fstat(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "fstat() requires an fd argument");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "fstat() fd must be a number");

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_STAT;
  req->promise = js_mkpromise(js);
  req->fd = (uv_file)(int)js_getnum(args[0]);
  req->uv_req.data = req;

  utarray_push_back(pending_requests, &req);
  int result = uv_fs_fstat(uv_default_loop(), &req->uv_req, req->fd, on_stat_complete);

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static ant_value_t builtin_fs_existsSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "existsSync() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "existsSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  struct stat st;
  int result = stat(path_cstr, &st);
  free(path_cstr);
  
  return js_bool(result == 0);
}

static ant_value_t builtin_fs_exists(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "exists() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "exists() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_EXISTS;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_stat(uv_default_loop(), &req->uv_req, req->path, on_exists_complete);
  
  if (result < 0) {
    req->completed = 1;
    js_resolve_promise(req->js, req->promise, js_false);
    remove_pending_request(req);
    free_fs_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_accessSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "accessSync() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "accessSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  int mode = F_OK;
  if (nargs >= 2 && vtype(args[1]) == T_NUM) {
    mode = (int)js_getnum(args[1]);
  }
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  int result = access(path_cstr, mode);
  
  if (result != 0) {
    ant_value_t err = fs_mk_errno_error(js, errno, "access", path_cstr, NULL);
    free(path_cstr); return err;
  }
  
  free(path_cstr);
  return js_mkundef();
}

static ant_value_t builtin_fs_chmodSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "chmodSync() requires a path argument");
  if (nargs < 2) return js_mkerr(js, "chmodSync() requires a mode argument");

  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "chmodSync() path must be a string or URL");

  mode_t mode = 0;
  if (!fs_parse_mode(js, args[1], &mode)) {
    return js_mkerr(js, "chmodSync() mode must be a number or octal string");
  }

  size_t path_len = 0;
  char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");

  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");

  uv_fs_t req;
  int result = uv_fs_chmod(uv_default_loop(), &req, path_cstr, mode, NULL);

  if (result < 0) {
    ant_value_t err = fs_mk_uv_error(js, result, "chmod", path_cstr, NULL);
    uv_fs_req_cleanup(&req);
    free(path_cstr);
    return err;
  }

  uv_fs_req_cleanup(&req);
  free(path_cstr);
  return js_mkundef();
}

static ant_value_t builtin_fs_chmod(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "chmod() requires a path argument");
  if (nargs < 2) return js_mkerr(js, "chmod() requires a mode argument");

  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "chmod() path must be a string or URL");

  mode_t mode = 0;
  if (!fs_parse_mode(js, args[1], &mode)) {
    return js_mkerr(js, "chmod() mode must be a number or octal string");
  }

  size_t path_len = 0;
  char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_CHMOD;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;

  if (!req->path) {
    free_fs_request(req);
    return js_mkerr(js, "Out of memory");
  }

  utarray_push_back(pending_requests, &req);
  int result = uv_fs_chmod(uv_default_loop(), &req->uv_req, req->path, mode, on_chmod_complete);

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static ant_value_t builtin_fs_access(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "access() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "access() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  int mode = F_OK;
  if (nargs >= 2 && vtype(args[1]) == T_NUM) {
    mode = (int)js_getnum(args[1]);
  }
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_ACCESS;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_access(uv_default_loop(), &req->uv_req, req->path, mode, on_access_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_realpathSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "realpathSync() requires a path argument");

  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "realpathSync() path must be a string");

  size_t path_len = 0;
  const char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");

  char *resolved = realpath(path, NULL);
  if (!resolved) return js_mkerr(js, "realpathSync failed for '%s'", path);

  ant_value_t result = js_mkstr(js, resolved, strlen(resolved));
  free(resolved);
  return result;
}

static ant_value_t builtin_fs_realpath(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "realpath() requires a path argument");

  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "realpath() path must be a string");

  size_t path_len = 0;
  const char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_REALPATH;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;

  if (!req->path) {
    free_fs_request(req);
    return js_mkerr(js, "Out of memory");
  }

  utarray_push_back(pending_requests, &req);
  int result = uv_fs_realpath(uv_default_loop(), &req->uv_req, req->path, on_realpath_complete);

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static ant_value_t builtin_fs_readlinkSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readlinkSync() requires a path argument");

  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "readlinkSync() path must be a string");

  size_t path_len = 0;
  const char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");

  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");

  uv_fs_t req;
  int result = uv_fs_readlink(NULL, &req, path_cstr, NULL);

  if (result < 0 || !req.ptr) {
    ant_value_t err = fs_mk_uv_error(js, result, "readlink", path_cstr, NULL);
    uv_fs_req_cleanup(&req);
    free(path_cstr);
    return err;
  }

  ant_value_t link = js_mkstr(js, (const char *)req.ptr, strlen((const char *)req.ptr));
  uv_fs_req_cleanup(&req);
  free(path_cstr);
  return link;
}

static ant_value_t builtin_fs_readlink(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readlink() requires a path argument");

  ant_value_t path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr(js, "readlink() path must be a string");

  size_t path_len = 0;
  const char *path = js_getstr(js, path_val, &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_REALPATH;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;

  if (!req->path) {
    free_fs_request(req);
    return js_mkerr(js, "Out of memory");
  }

  utarray_push_back(pending_requests, &req);
  int result = uv_fs_readlink(uv_default_loop(), &req->uv_req, req->path, on_realpath_complete);

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static ant_value_t builtin_fs_readdirSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readdirSync() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "readdirSync() path must be a string");
  
  bool with_file_types = false;
  if (nargs >= 2 && vtype(args[1]) == T_OBJ) {
    ant_value_t wft = js_get(js, args[1], "withFileTypes");
    with_file_types = js_truthy(js, wft);
  }

  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  uv_fs_t req;
  int result = uv_fs_scandir(NULL, &req, path_cstr, 0, NULL);
  
  if (result < 0) {
    ant_value_t err = fs_mk_uv_error(js, result, "scandir", path_cstr, NULL);
    uv_fs_req_cleanup(&req);
    free(path_cstr);
    return err;
  }
  
  free(path_cstr);
  
  ant_value_t arr = js_mkarr(js);
  uv_dirent_t dirent;
  
  while (uv_fs_scandir_next(&req, &dirent) != UV_EOF) {
  if (with_file_types) {
    ant_value_t entry = create_dirent_object(js, dirent.name, strlen(dirent.name), dirent.type);
    js_arr_push(js, arr, entry);
  } else {
    ant_value_t name = js_mkstr(js, dirent.name, strlen(dirent.name));
    js_arr_push(js, arr, name);
  }}
  
  uv_fs_req_cleanup(&req);
  return arr;
}

static ant_value_t builtin_fs_readdir(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "readdir() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "readdir() path must be a string");
  
  bool with_file_types = false;
  if (nargs >= 2 && vtype(args[1]) == T_OBJ) {
    ant_value_t wft = js_get(js, args[1], "withFileTypes");
    with_file_types = js_truthy(js, wft);
  }

  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_READDIR;
  req->with_file_types = with_file_types;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_scandir(uv_default_loop(), &req->uv_req, req->path, 0, on_readdir_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static void on_write_fd_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  req->completed = 1;
  js_resolve_promise(req->js, req->promise, js_mknum((double)uv_req->result));
  remove_pending_request(req);
  free_fs_request(req);
}

static void on_read_fd_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;

  if (uv_req->result < 0) {
    if (is_callable(req->callback_fn)) {
      ant_value_t err = js_mkerr(req->js, "read failed: %s", uv_strerror((int)uv_req->result));
      ant_value_t cb_args[1] = { err };
      fs_call_value(req->js, req->callback_fn, js_mkundef(), cb_args, 1);
      remove_pending_request(req);
      free_fs_request(req);
      return;
    }
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }

  ssize_t bytes_read = uv_req->result;

  if (req->data && bytes_read > 0) {
    TypedArrayData *ta = buffer_get_typedarray_data(req->target_buffer);
    if (ta && ta->buffer && ta->buffer->data) {
      uint8_t *dest = ta->buffer->data + ta->byte_offset + req->buf_offset;
      memcpy(dest, req->data, (size_t)bytes_read);
    }
  }

  if (is_callable(req->callback_fn)) {
    ant_value_t cb_args[3] = { js_mknull(), js_mknum((double)bytes_read), req->target_buffer };
    fs_call_value(req->js, req->callback_fn, js_mkundef(), cb_args, 3);
    remove_pending_request(req);
    free_fs_request(req);
    return;
  }

  req->completed = 1;
  js_resolve_promise(req->js, req->promise, js_mknum((double)bytes_read));
  remove_pending_request(req);
  free_fs_request(req);
}

static ant_value_t builtin_fs_read_fd(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "read() requires fd and buffer arguments");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "read() fd must be a number");

  int fd = (int)js_getnum(args[0]);

  ant_value_t buf_arg = args[1];
  TypedArrayData *ta_data = buffer_get_typedarray_data(buf_arg);
  if (!ta_data || !ta_data->buffer || !ta_data->buffer->data)
    return js_mkerr(js, "read() buffer argument must be a Buffer or TypedArray");

  size_t buf_len = ta_data->byte_length;
  size_t offset = 0;
  size_t length = buf_len;
  int64_t position = -1;

  ant_value_t callback = js_mkundef();
  int data_nargs = nargs;
  if (nargs >= 3 && is_callable(args[nargs - 1])) {
    callback = args[nargs - 1];
    data_nargs = nargs - 1;
  }

  if (data_nargs >= 3 && vtype(args[2]) == T_NUM) offset = (size_t)js_getnum(args[2]);
  if (data_nargs >= 4 && vtype(args[3]) == T_NUM) length = (size_t)js_getnum(args[3]);
  if (data_nargs >= 5 && vtype(args[4]) == T_NUM) position = (int64_t)js_getnum(args[4]);

  if (offset > buf_len) return js_mkerr(js, "read() offset out of bounds");
  if (offset + length > buf_len) return js_mkerr(js, "read() length extends beyond buffer");

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_READ_FD;
  req->promise = js_mkpromise(js);
  req->fd = fd;
  req->target_buffer = buf_arg;
  req->buf_offset = offset;
  req->data_len = length;
  req->callback_fn = callback;
  req->data = malloc(length > 0 ? length : 1);
  if (!req->data) {
    free(req);
    return js_mkerr(js, "Out of memory");
  }
  req->uv_req.data = req;

  utarray_push_back(pending_requests, &req);

  uv_buf_t buf = uv_buf_init(req->data, (unsigned int)length);
  int result = uv_fs_read(uv_default_loop(), &req->uv_req, req->fd, &buf, 1, position, on_read_fd_complete);

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static ant_value_t builtin_fs_readSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "readSync() requires fd and buffer arguments");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "readSync() fd must be a number");
  
  int fd = (int)js_getnum(args[0]);
  
  TypedArrayData *ta_data = buffer_get_typedarray_data(args[1]);
  if (!ta_data || !ta_data->buffer || !ta_data->buffer->data)
    return js_mkerr(js, "readSync() second argument must be a Buffer, TypedArray, or DataView");
  
  uint8_t *buf_data = ta_data->buffer->data + ta_data->byte_offset;
  size_t buf_len = ta_data->byte_length;
  size_t offset = 0;
  size_t length = buf_len;
  int64_t position = -1;
  
  if (nargs >= 3) {
  if (vtype(args[2]) == T_OBJ) {
    ant_value_t off_val = js_get(js, args[2], "offset");
    ant_value_t len_val = js_get(js, args[2], "length");
    ant_value_t pos_val = js_get(js, args[2], "position");
    if (vtype(off_val) == T_NUM) offset = (size_t)js_getnum(off_val);
    if (vtype(len_val) == T_NUM) length = (size_t)js_getnum(len_val);
    else length = buf_len - offset;
    if (vtype(pos_val) == T_NUM) position = (int64_t)js_getnum(pos_val);
  } else if (vtype(args[2]) == T_NUM) {
    offset = (size_t)js_getnum(args[2]);
    length = buf_len - offset;
    if (nargs >= 4 && vtype(args[3]) == T_NUM) length = (size_t)js_getnum(args[3]);
    if (nargs >= 5 && vtype(args[4]) == T_NUM) position = (int64_t)js_getnum(args[4]);
  }}
  
  if (offset > buf_len) return js_mkerr(js, "offset is out of bounds");
  if (offset + length > buf_len) return js_mkerr(js, "length extends beyond buffer");
  
  uv_fs_t req;
  uv_buf_t buf = uv_buf_init((char *)(buf_data + offset), (unsigned int)length);
  int result = uv_fs_read(uv_default_loop(), &req, fd, &buf, 1, position, NULL);
  uv_fs_req_cleanup(&req);
  
  if (result < 0) return js_mkerr(js, "readSync failed: %s", uv_strerror(result));
  return js_mknum((double)result);
}

static ant_value_t builtin_fs_writeSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "writeSync() requires fd and data arguments");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "writeSync() fd must be a number");
  
  int fd = (int)js_getnum(args[0]);
  
  if (vtype(args[1]) == T_STR) {
    size_t str_len;
    const char *str = js_getstr(js, args[1], &str_len);
    if (!str) return js_mkerr(js, "Failed to get string");
    
    int64_t position = -1;
    if (nargs >= 3 && vtype(args[2]) == T_NUM)
      position = (int64_t)js_getnum(args[2]);
    
    uv_fs_t req;
    uv_buf_t buf = uv_buf_init((char *)str, (unsigned int)str_len);
    int result = uv_fs_write(uv_default_loop(), &req, fd, &buf, 1, position, NULL);
    uv_fs_req_cleanup(&req);
    
    if (result < 0) return js_mkerr(js, "writeSync failed: %s", uv_strerror(result));
    return js_mknum((double)result);
  }
  
  TypedArrayData *ta_data = buffer_get_typedarray_data(args[1]);
  if (!ta_data || !ta_data->buffer || !ta_data->buffer->data)
    return js_mkerr(js, "writeSync() second argument must be a Buffer, TypedArray, DataView, or string");
  
  uint8_t *buf_data = ta_data->buffer->data + ta_data->byte_offset;
  size_t buf_len = ta_data->byte_length;
  size_t offset = 0;
  size_t length = buf_len;
  int64_t position = -1;
  
  if (nargs >= 3) {
    if (vtype(args[2]) == T_OBJ) {
      ant_value_t off_val = js_get(js, args[2], "offset");
      ant_value_t len_val = js_get(js, args[2], "length");
      ant_value_t pos_val = js_get(js, args[2], "position");
      if (vtype(off_val) == T_NUM) offset = (size_t)js_getnum(off_val);
      if (vtype(len_val) == T_NUM) length = (size_t)js_getnum(len_val);
      else length = buf_len - offset;
      if (vtype(pos_val) == T_NUM) position = (int64_t)js_getnum(pos_val);
    } else if (vtype(args[2]) == T_NUM) {
      offset = (size_t)js_getnum(args[2]);
      length = buf_len - offset;
      if (nargs >= 4 && vtype(args[3]) == T_NUM) length = (size_t)js_getnum(args[3]);
      if (nargs >= 5 && vtype(args[4]) == T_NUM) position = (int64_t)js_getnum(args[4]);
    }
  }
  
  if (offset > buf_len) return js_mkerr(js, "offset is out of bounds");
  if (offset + length > buf_len) return js_mkerr(js, "length extends beyond buffer");
  
  uv_fs_t req;
  uv_buf_t buf = uv_buf_init((char *)(buf_data + offset), (unsigned int)length);
  int result = uv_fs_write(uv_default_loop(), &req, fd, &buf, 1, position, NULL);
  uv_fs_req_cleanup(&req);
  
  if (result < 0) return js_mkerr(js, "writeSync failed: %s", uv_strerror(result));
  return js_mknum((double)result);
}

static ant_value_t builtin_fs_write_fd(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "write() requires fd and data arguments");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "write() fd must be a number");
  
  int fd = (int)js_getnum(args[0]);
  const char *write_data;
  size_t write_len;
  int64_t position = -1;
  
  if (vtype(args[1]) == T_STR) {
    size_t str_len;
    const char *str = js_getstr(js, args[1], &str_len);
    if (!str) return js_mkerr(js, "Failed to get string");
    
    if (nargs >= 3 && vtype(args[2]) == T_NUM)
      position = (int64_t)js_getnum(args[2]);
    
    write_data = str;
    write_len = str_len;
  } else {
    TypedArrayData *ta_data = buffer_get_typedarray_data(args[1]);
    if (!ta_data || !ta_data->buffer || !ta_data->buffer->data)
      return js_mkerr(js, "write() second argument must be a Buffer, TypedArray, DataView, or string");
    
    uint8_t *buf_data = ta_data->buffer->data + ta_data->byte_offset;
    size_t buf_len = ta_data->byte_length;
    size_t offset = 0;
    size_t length = buf_len;
    
    if (nargs >= 3) {
    if (vtype(args[2]) == T_OBJ) {
      ant_value_t off_val = js_get(js, args[2], "offset");
      ant_value_t len_val = js_get(js, args[2], "length");
      ant_value_t pos_val = js_get(js, args[2], "position");
      if (vtype(off_val) == T_NUM) offset = (size_t)js_getnum(off_val);
      if (vtype(len_val) == T_NUM) length = (size_t)js_getnum(len_val);
      else length = buf_len - offset;
      if (vtype(pos_val) == T_NUM) position = (int64_t)js_getnum(pos_val);
    } else if (vtype(args[2]) == T_NUM) {
      offset = (size_t)js_getnum(args[2]);
      length = buf_len - offset;
      if (nargs >= 4 && vtype(args[3]) == T_NUM) length = (size_t)js_getnum(args[3]);
      if (nargs >= 5 && vtype(args[4]) == T_NUM) position = (int64_t)js_getnum(args[4]);
    }}
    
    if (offset > buf_len) return js_mkerr(js, "offset is out of bounds");
    if (offset + length > buf_len) return js_mkerr(js, "length extends beyond buffer");
    
    write_data = (const char *)(buf_data + offset);
    write_len = length;
  }
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_WRITE_FD;
  req->promise = js_mkpromise(js);
  req->fd = fd;
  req->data = malloc(write_len);
  if (!req->data) {
    free(req);
    return js_mkerr(js, "Out of memory");
  }
  memcpy(req->data, write_data, write_len);
  req->data_len = write_len;
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  
  uv_buf_t buf = uv_buf_init(req->data, (unsigned int)req->data_len);
  int result = uv_fs_write(uv_default_loop(), &req->uv_req, req->fd, &buf, 1, position, on_write_fd_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_writevSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "writevSync() requires fd and buffers arguments");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "writevSync() fd must be a number");
  
  int fd = (int)js_getnum(args[0]);
  ant_offset_t arr_len = js_arr_len(js, args[1]);
  if (arr_len == 0) return js_mknum(0);
  
  int64_t position = -1;
  if (nargs >= 3 && vtype(args[2]) == T_NUM)
    position = (int64_t)js_getnum(args[2]);
  
  uv_buf_t *bufs = calloc((size_t)arr_len, sizeof(uv_buf_t));
  if (!bufs) return js_mkerr(js, "Out of memory");
  
  for (ant_offset_t i = 0; i < arr_len; i++) {
    ant_value_t item = js_arr_get(js, args[1], i);
    TypedArrayData *ta = buffer_get_typedarray_data(item);
    if (!ta || !ta->buffer || !ta->buffer->data) {
      free(bufs);
      return js_mkerr(js, "writevSync() buffers must contain ArrayBufferViews");
    }
    bufs[i] = uv_buf_init((char *)(ta->buffer->data + ta->byte_offset), (unsigned int)ta->byte_length);
  }
  
  uv_fs_t req;
  int result = uv_fs_write(uv_default_loop(), &req, fd, bufs, (unsigned int)arr_len, position, NULL);
  uv_fs_req_cleanup(&req);
  free(bufs);
  
  if (result < 0) return js_mkerr(js, "writevSync failed: %s", uv_strerror(result));
  return js_mknum((double)result);
}

static ant_value_t builtin_fs_writev_fd(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "writev() requires fd and buffers arguments");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "writev() fd must be a number");
  
  int fd = (int)js_getnum(args[0]);
  ant_offset_t arr_len = js_arr_len(js, args[1]);
  
  if (arr_len == 0) {
    ant_value_t promise = js_mkpromise(js);
    js_resolve_promise(js, promise, js_mknum(0));
    return promise;
  }
  
  int64_t position = -1;
  if (nargs >= 3 && vtype(args[2]) == T_NUM)
    position = (int64_t)js_getnum(args[2]);
  
  size_t total_len = 0;
  for (ant_offset_t i = 0; i < arr_len; i++) {
    ant_value_t item = js_arr_get(js, args[1], i);
    TypedArrayData *ta = buffer_get_typedarray_data(item);
    if (!ta || !ta->buffer || !ta->buffer->data)
      return js_mkerr(js, "writev() buffers must contain ArrayBufferViews");
    total_len += ta->byte_length;
  }
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->data = malloc(total_len);
  if (!req->data) {
    free(req);
    return js_mkerr(js, "Out of memory");
  }
  
  size_t off = 0;
  for (ant_offset_t i = 0; i < arr_len; i++) {
    ant_value_t item = js_arr_get(js, args[1], i);
    TypedArrayData *ta = buffer_get_typedarray_data(item);
    memcpy(req->data + off, ta->buffer->data + ta->byte_offset, ta->byte_length);
    off += ta->byte_length;
  }
  
  req->js = js;
  req->op_type = FS_OP_WRITE_FD;
  req->promise = js_mkpromise(js);
  req->fd = fd;
  req->data_len = total_len;
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  
  uv_buf_t buf = uv_buf_init(req->data, (unsigned int)total_len);
  int result = uv_fs_write(uv_default_loop(), &req->uv_req, req->fd, &buf, 1, position, on_write_fd_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static int parse_open_flags(ant_t *js, ant_value_t arg) {
  if (vtype(arg) == T_NUM) return (int)js_getnum(arg);
  if (vtype(arg) != T_STR) return O_RDONLY;
  
  size_t len;
  const char *str = js_getstr(js, arg, &len);
  if (!str) return O_RDONLY;
  
  if (len == 1 && str[0] == 'r')                      return O_RDONLY;
  if (len == 2 && memcmp(str, "r+", 2) == 0)          return O_RDWR;
  if (len == 1 && str[0] == 'w')                      return O_WRONLY | O_CREAT | O_TRUNC;
  if (len == 2 && memcmp(str, "wx", 2) == 0)          return O_WRONLY | O_CREAT | O_TRUNC | O_EXCL;
  if (len == 2 && memcmp(str, "w+", 2) == 0)          return O_RDWR | O_CREAT | O_TRUNC;
  if (len == 3 && memcmp(str, "wx+", 3) == 0)         return O_RDWR | O_CREAT | O_TRUNC | O_EXCL;
  if (len == 1 && str[0] == 'a')                      return O_WRONLY | O_CREAT | O_APPEND;
  if (len == 2 && memcmp(str, "ax", 2) == 0)          return O_WRONLY | O_CREAT | O_APPEND | O_EXCL;
  if (len == 2 && memcmp(str, "a+", 2) == 0)          return O_RDWR | O_CREAT | O_APPEND;
  if (len == 3 && memcmp(str, "ax+", 3) == 0)         return O_RDWR | O_CREAT | O_APPEND | O_EXCL;
  
  return O_RDONLY;
}

static ant_value_t builtin_fs_openSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "openSync() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "openSync() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  int flags = (nargs >= 2) ? parse_open_flags(js, args[1]) : O_RDONLY;
  int mode = (nargs >= 3 && vtype(args[2]) == T_NUM) ? (int)js_getnum(args[2]) : 0666;
  
  char *path_cstr = strndup(path, path_len);
  if (!path_cstr) return js_mkerr(js, "Out of memory");
  
  uv_fs_t req;
  int result = uv_fs_open(uv_default_loop(), &req, path_cstr, flags, mode, NULL);
  
  if (result < 0) {
    ant_value_t err = fs_mk_uv_error(js, result, "open", path_cstr, NULL);
    uv_fs_req_cleanup(&req);
    free(path_cstr);
    return err;
  }
  
  uv_fs_req_cleanup(&req);
  free(path_cstr);
  
  return js_mknum((double)result);
}

static ant_value_t builtin_fs_closeSync(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "closeSync() requires a fd argument");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "closeSync() fd must be a number");
  
  int fd = (int)js_getnum(args[0]);
  
  uv_fs_t req;
  int result = uv_fs_close(uv_default_loop(), &req, fd, NULL);
  uv_fs_req_cleanup(&req);
  
  if (result < 0) return js_mkerr(js, "closeSync failed: %s", uv_strerror(result));
  return js_mkundef();
}

static void on_open_fd_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  req->completed = 1;
  js_resolve_promise(req->js, req->promise, js_mknum((double)uv_req->result));
  remove_pending_request(req);
  free_fs_request(req);
}

static void on_open_filehandle_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;

  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }

  req->completed = 1;
  js_resolve_promise(
    req->js, req->promise, 
    fs_make_filehandle(req->js, (int)uv_req->result)
  );
  
  remove_pending_request(req);
  free_fs_request(req);
}

static ant_value_t builtin_fs_open_fd(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "open() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "open() path must be a string");
  
  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");
  
  int flags = (nargs >= 2) ? parse_open_flags(js, args[1]) : O_RDONLY;
  int mode = (nargs >= 3 && vtype(args[2]) == T_NUM) ? (int)js_getnum(args[2]) : 0666;
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_OPEN;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_open(uv_default_loop(), &req->uv_req, req->path, flags, mode, on_open_fd_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_open_filehandle(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "open() requires a path argument");
  if (vtype(args[0]) != T_STR) return js_mkerr(js, "open() path must be a string");

  size_t path_len;
  char *path = js_getstr(js, args[0], &path_len);
  if (!path) return js_mkerr(js, "Failed to get path string");

  int flags = (nargs >= 2) ? parse_open_flags(js, args[1]) : O_RDONLY;
  int mode = (nargs >= 3 && vtype(args[2]) == T_NUM) ? (int)js_getnum(args[2]) : 0666;

  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");

  req->js = js;
  req->op_type = FS_OP_OPEN;
  req->promise = js_mkpromise(js);
  req->path = strndup(path, path_len);
  req->uv_req.data = req;

  utarray_push_back(pending_requests, &req);
  int result = uv_fs_open(
    uv_default_loop(), &req->uv_req, req->path, 
    flags, mode, on_open_filehandle_complete
  );

  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }

  return req->promise;
}

static void on_close_fd_complete(uv_fs_t *uv_req) {
  fs_request_t *req = (fs_request_t *)uv_req->data;
  
  if (uv_req->result < 0) {
    fs_request_fail(req, (int)uv_req->result);
    req->completed = 1;
    complete_request(req);
    return;
  }
  
  req->completed = 1;
  js_resolve_promise(req->js, req->promise, js_mkundef());
  remove_pending_request(req);
  free_fs_request(req);
}

static ant_value_t builtin_fs_close_fd(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "close() requires a fd argument");
  if (vtype(args[0]) != T_NUM) return js_mkerr(js, "close() fd must be a number");
  
  int fd = (int)js_getnum(args[0]);
  
  fs_request_t *req = calloc(1, sizeof(fs_request_t));
  if (!req) return js_mkerr(js, "Out of memory");
  
  req->js = js;
  req->op_type = FS_OP_CLOSE;
  req->promise = js_mkpromise(js);
  req->fd = fd;
  req->uv_req.data = req;
  
  utarray_push_back(pending_requests, &req);
  int result = uv_fs_close(uv_default_loop(), &req->uv_req, req->fd, on_close_fd_complete);
  
  if (result < 0) {
    fs_request_fail(req, result);
    req->completed = 1;
    complete_request(req);
  }
  
  return req->promise;
}

static ant_value_t builtin_fs_watch(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t path_val = 0;
  ant_value_t watcher_obj = 0;
  fs_watch_options_t opts;
  fs_watcher_t *watcher = NULL;
  uv_handle_t *handle = NULL;
  
  const char *path = NULL;
  size_t path_len = 0;
  int rc = 0;

  if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "watch() requires a path argument");
  if (!fs_parse_watch_options(js, args, nargs, &opts))
    return js_mkerr_typed(js, JS_ERR_TYPE, "watch() options must be a string, object, or callback");

  path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr_typed(js, JS_ERR_TYPE, "watch() path must be a string or URL");

  path = js_getstr(js, path_val, &path_len);
  if (!path || path_len == 0) return js_mkerr(js, "watch() path must not be empty");

  watcher = fs_watcher_new(js, FS_WATCH_MODE_EVENT);
  if (!watcher) return js_mkerr(js, "Out of memory");

  watcher_obj = fs_watcher_make_object(js, watcher);
  if (is_err(watcher_obj)) {
    fs_watcher_free(watcher);
    return watcher_obj;
  }

  rc = fs_watcher_start_event(watcher, path, opts.persistent, opts.recursive);
  if (rc != 0) {
    js_set_native_ptr(watcher_obj, NULL);
    fs_watcher_free(watcher);
    return fs_watch_error(js, rc, path);
  }

  fs_add_active_watcher(watcher);
  if (!opts.persistent) {
    handle = fs_watcher_uv_handle(watcher);
    if (handle) uv_unref(handle);
  }
  if (is_callable(opts.listener))
    eventemitter_add_listener(js, watcher_obj, "change", opts.listener, false);
  return watcher_obj;
}

static ant_value_t builtin_fs_watchFile(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t path_val = 0;
  fs_watchfile_options_t opts;
  
  fs_watcher_t *watcher = NULL;
  uv_handle_t *handle = NULL;
  
  const char *path = NULL;
  size_t path_len = 0;
  int rc = 0;

  if (nargs < 2)
    return js_mkerr_typed(js, JS_ERR_TYPE, "watchFile() requires a path and listener");
  if (!fs_parse_watchfile_options(js, args, nargs, &opts))
    return js_mkerr_typed(js, JS_ERR_TYPE, "watchFile() requires a listener callback");

  path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkerr_typed(js, JS_ERR_TYPE, "watchFile() path must be a string or URL");

  path = js_getstr(js, path_val, &path_len);
  if (!path || path_len == 0) return js_mkerr(js, "watchFile() path must not be empty");

  watcher = fs_watcher_new(js, FS_WATCH_MODE_STAT);
  if (!watcher) return js_mkerr(js, "Out of memory");

  watcher->callback = opts.listener;
  watcher->last_stat_valid = fs_stat_path_sync(path, &watcher->last_stat);

  rc = fs_watcher_start_poll(watcher, path, opts.persistent, opts.interval_ms);
  if (rc != 0) {
    fs_watcher_free(watcher);
    return fs_watch_error(js, rc, path);
  }

  fs_add_active_watcher(watcher);
  if (!opts.persistent) {
    handle = fs_watcher_uv_handle(watcher);
    if (handle) uv_unref(handle);
  }
  return js_mkundef();
}

static ant_value_t builtin_fs_unwatchFile(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t path_val = 0;
  ant_value_t listener = js_mkundef();
  
  char *resolved = NULL;
  fs_watcher_t *watcher = NULL;
  fs_watcher_t *next = NULL;
  
  const char *path = NULL;
  size_t path_len = 0;

  if (nargs < 1) return js_mkundef();

  path_val = fs_coerce_path(js, args[0]);
  if (vtype(path_val) != T_STR) return js_mkundef();

  path = js_getstr(js, path_val, &path_len);
  if (!path || path_len == 0) return js_mkundef();
  if (nargs > 1 && is_callable(args[1])) listener = args[1];

  resolved = ant_watch_resolve_path(path);
  if (!resolved) return js_mkundef();

  for (watcher = active_watchers; watcher; watcher = next) {
    next = watcher->next_active;
    if (watcher->mode != FS_WATCH_MODE_STAT) continue;
    if (!watcher->path || strcmp(watcher->path, resolved) != 0) continue;
    if (is_callable(listener) && watcher->callback != listener) continue;
    fs_watcher_close_native(watcher);
  }

  free(resolved);
  return js_mkundef();
}

void init_fs_module(void) {
  utarray_new(pending_requests, &ut_ptr_icd);
  
  ant_t *js = rt->js;
  ant_value_t glob = js->global;
  
  ant_value_t stats_ctor = js_mkobj(js);
  ant_value_t stats_proto = js_mkobj(js);
  
  js_set(js, stats_proto, "isFile", js_mkfun(stat_isFile));
  js_set(js, stats_proto, "isDirectory", js_mkfun(stat_isDirectory));
  js_set(js, stats_proto, "isSymbolicLink", js_mkfun(stat_isSymbolicLink));
  js_set_sym(js, stats_proto, get_toStringTag_sym(), js_mkstr(js, "Stats", 5));
  
  js_mkprop_fast(js, stats_ctor, "prototype", 9, stats_proto);
  js_mkprop_fast(js, stats_ctor, "name", 4, js_mkstr(js, "Stats", 5));
  js_set_descriptor(js, stats_ctor, "name", 4, 0);
  
  js_set(js, glob, "Stats", js_obj_to_func(stats_ctor));

  g_dirent_proto = js_mkobj(js);
  js_set(js, g_dirent_proto, "isFile", js_mkfun(dirent_isFile));
  js_set(js, g_dirent_proto, "isDirectory", js_mkfun(dirent_isDirectory));
  js_set(js, g_dirent_proto, "isSymbolicLink", js_mkfun(dirent_isSymbolicLink));
  js_set(js, g_dirent_proto, "isBlockDevice", js_mkfun(dirent_isBlockDevice));
  js_set(js, g_dirent_proto, "isCharacterDevice", js_mkfun(dirent_isCharacterDevice));
  js_set(js, g_dirent_proto, "isFIFO", js_mkfun(dirent_isFIFO));
  js_set(js, g_dirent_proto, "isSocket", js_mkfun(dirent_isSocket));
  js_set_sym(js, g_dirent_proto, get_toStringTag_sym(), js_mkstr(js, "Dirent", 6));
  gc_register_root(&g_dirent_proto);
}

static ant_value_t fs_callback_success_handler(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t ctx = js_get_slot(js_getcurrentfunc(js), SLOT_DATA);
  ant_value_t callback = js_get_slot(ctx, SLOT_DATA);

  if (!is_callable(callback)) return js_mkundef();

  if (js_truthy(js, js_get(js, ctx, "existsStyle"))) {
    ant_value_t cb_args[1] = { nargs > 0 ? args[0] : js_false };
    fs_call_value(js, callback, js_mkundef(), cb_args, 1);
    return js_mkundef();
  }

  ant_value_t cb_args[2] = { js_mknull(), nargs > 0 ? args[0] : js_mkundef() };
  fs_call_value(js, callback, js_mkundef(), cb_args, 2);
  return js_mkundef();
}

static ant_value_t fs_callback_error_handler(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t ctx = js_get_slot(js_getcurrentfunc(js), SLOT_DATA);
  ant_value_t callback = js_get_slot(ctx, SLOT_DATA);
  ant_value_t cb_args[1];

  if (!is_callable(callback)) return js_mkundef();

  if (js_truthy(js, js_get(js, ctx, "existsStyle"))) {
    cb_args[0] = js_false;
    fs_call_value(js, callback, js_mkundef(), cb_args, 1);
    return js_mkundef();
  }

  cb_args[0] = nargs > 0 ? args[0] : js_mkundef();
  fs_call_value(js, callback, js_mkundef(), cb_args, 1);
  return js_mkundef();
}

static void fs_callback_emit_success(
  ant_t *js,
  ant_value_t callback,
  bool exists_style,
  ant_value_t value
) {
  if (exists_style) {
    ant_value_t cb_args[1] = { value };
    fs_call_value(js, callback, js_mkundef(), cb_args, 1);
    return;
  }

  ant_value_t cb_args[2] = { js_mknull(), value };
  fs_call_value(js, callback, js_mkundef(), cb_args, 2);
}

static void fs_callback_emit_error(
  ant_t *js,
  ant_value_t callback,
  bool exists_style,
  ant_value_t error
) {
  ant_value_t cb_args[1];

  cb_args[0] = exists_style ? js_false : error;
  fs_call_value(js, callback, js_mkundef(), cb_args, 1);
}

static ant_value_t fs_callback_attach_promise(
  ant_t *js,
  ant_value_t promise,
  ant_value_t callback,
  bool exists_style
) {
  GC_ROOT_SAVE(root_mark, js);
  ant_value_t success_fn = js_mkundef();
  ant_value_t error_fn = js_mkundef();

  GC_ROOT_PIN(js, promise);
  GC_ROOT_PIN(js, callback);

  ant_value_t success_ctx = js_mkobj(js);
  GC_ROOT_PIN(js, success_ctx);
  ant_value_t error_ctx = js_mkobj(js);
  GC_ROOT_PIN(js, error_ctx);

  js_set_slot(success_ctx, SLOT_DATA, callback);
  js_set_slot(error_ctx, SLOT_DATA, callback);
  if (exists_style) {
    js_set(js, success_ctx, "existsStyle", js_true);
    js_set(js, error_ctx, "existsStyle", js_true);
  }

  success_fn = js_heavy_mkfun(js, fs_callback_success_handler, success_ctx);
  GC_ROOT_PIN(js, success_fn);
  error_fn = js_heavy_mkfun(js, fs_callback_error_handler, error_ctx);
  GC_ROOT_PIN(js, error_fn);

  js_promise_then(js, promise, success_fn, error_fn);
  GC_ROOT_RESTORE(js, root_mark);
  return js_mkundef();
}

static ant_value_t fs_callback_wrapper_call(ant_t *js, ant_value_t *args, int nargs) {
  GC_ROOT_SAVE(root_mark, js);
  ant_value_t wrapper = js_getcurrentfunc(js);
  ant_value_t config = js_get_slot(wrapper, SLOT_DATA);
  ant_value_t original = js_get_slot(config, SLOT_DATA);
  
  ant_value_t callback = js_mkundef();
  ant_value_t result = js_mkundef();
  ant_value_t this_arg = js_getthis(js);
  ant_value_t ex = js_mkundef();
  
  bool exists_style = false;
  int call_nargs = nargs;

  GC_ROOT_PIN(js, original);
  GC_ROOT_PIN(js, this_arg);

  exists_style = js_truthy(js, js_get(js, config, "existsStyle"));
  if (nargs > 0 && is_callable(args[nargs - 1])) {
    callback = args[nargs - 1];
    GC_ROOT_PIN(js, callback);
    call_nargs--;
  }

  result = fs_call_value(js, original, this_arg, args, call_nargs);
  GC_ROOT_PIN(js, result);

  if (!is_callable(callback)) {
    GC_ROOT_RESTORE(js, root_mark);
    return result;
  }

  if (is_err(result) || js->thrown_exists) {
    ex = js->thrown_exists ? js->thrown_value : result;
    GC_ROOT_PIN(js, ex);
    js->thrown_exists = false;
    js->thrown_value = js_mkundef();
    js->thrown_stack = js_mkundef();
    fs_callback_emit_error(js, callback, exists_style, ex);
    GC_ROOT_RESTORE(js, root_mark);
    return js_mkundef();
  }

  if (vtype(result) != T_PROMISE) {
    fs_callback_emit_success(js, callback, exists_style, result);
    GC_ROOT_RESTORE(js, root_mark);
    return js_mkundef();
  }

  ant_value_t attach_result = fs_callback_attach_promise(js, result, callback, exists_style);
  GC_ROOT_RESTORE(js, root_mark);
  return attach_result;
}

static ant_value_t fs_make_callback_wrapper(ant_t *js, ant_value_t original, bool exists_style) {
  ant_value_t config = js_mkobj(js);

  js_set_slot(config, SLOT_DATA, original);
  if (exists_style) js_set(js, config, "existsStyle", js_true);
  return js_heavy_mkfun(js, fs_callback_wrapper_call, config);
}

static void fs_set_promise_methods(ant_t *js, ant_value_t lib) {
  js_set(js, lib, "appendFile", js_mkfun(builtin_fs_appendFile));
  js_set(js, lib, "cp", js_mkfun(builtin_fs_cp));
  js_set(js, lib, "copyFile", js_mkfun(builtin_fs_copyFile));
  js_set(js, lib, "readFile", js_mkfun(builtin_fs_readFile));
  js_set(js, lib, "open", js_mkfun(builtin_fs_open_filehandle));
  js_set(js, lib, "close", js_mkfun(builtin_fs_close_fd));
  js_set(js, lib, "writeFile", js_mkfun(builtin_fs_writeFile));
  js_set(js, lib, "write", js_mkfun(builtin_fs_write_fd));
  js_set(js, lib, "writev", js_mkfun(builtin_fs_writev_fd));
  js_set(js, lib, "rename", js_mkfun(builtin_fs_rename));
  js_set(js, lib, "rm", js_mkfun(builtin_fs_rm));
  js_set(js, lib, "unlink", js_mkfun(builtin_fs_unlink));
  js_set(js, lib, "mkdir", js_mkfun(builtin_fs_mkdir));
  js_set(js, lib, "mkdtemp", js_mkfun(builtin_fs_mkdtemp));
  js_set(js, lib, "rmdir", js_mkfun(builtin_fs_rmdir));
  js_set(js, lib, "stat", js_mkfun(builtin_fs_stat));
  js_set(js, lib, "lstat", js_mkfun(builtin_fs_lstat));
  js_set(js, lib, "fstat", js_mkfun(builtin_fs_fstat));
  js_set(js, lib, "utimes", js_mkfun(builtin_fs_utimes));
  js_set(js, lib, "futimes", js_mkfun(builtin_fs_futimes));
  js_set(js, lib, "exists", js_mkfun(builtin_fs_exists));
  js_set(js, lib, "access", js_mkfun(builtin_fs_access));
  js_set(js, lib, "chmod", js_mkfun(builtin_fs_chmod));
  js_set(js, lib, "readdir", js_mkfun(builtin_fs_readdir));
  js_set(js, lib, "realpath", js_mkfun(builtin_fs_realpath));
  js_set(js, lib, "readlink", js_mkfun(builtin_fs_readlink));
}

static void fs_set_callback_compatible_methods(ant_t *js, ant_value_t lib) {
  ant_value_t realpath = fs_make_callback_wrapper(js, js_mkfun(builtin_fs_realpath), false);

  js_set(js, lib, "appendFile", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_appendFile), false));
  js_set(js, lib, "cp", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_cp), false));
  js_set(js, lib, "copyFile", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_copyFile), false));
  js_set(js, lib, "readFile", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_readFile), false));
  js_set(js, lib, "open", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_open_fd), false));
  js_set(js, lib, "close", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_close_fd), false));
  js_set(js, lib, "writeFile", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_writeFile), false));
  js_set(js, lib, "write", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_write_fd), false));
  js_set(js, lib, "writev", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_writev_fd), false));
  js_set(js, lib, "rename", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_rename), false));
  js_set(js, lib, "rm", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_rm), false));
  js_set(js, lib, "unlink", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_unlink), false));
  js_set(js, lib, "mkdir", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_mkdir), false));
  js_set(js, lib, "mkdtemp", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_mkdtemp), false));
  js_set(js, lib, "rmdir", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_rmdir), false));
  js_set(js, lib, "stat", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_stat), false));
  js_set(js, lib, "lstat", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_lstat), false));
  js_set(js, lib, "fstat", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_fstat), false));
  js_set(js, lib, "utimes", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_utimes), false));
  js_set(js, lib, "futimes", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_futimes), false));
  js_set(js, lib, "exists", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_exists), true));
  js_set(js, lib, "access", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_access), false));
  js_set(js, lib, "chmod", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_chmod), false));
  js_set(js, lib, "readdir", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_readdir), false));
  js_set(js, lib, "readlink", fs_make_callback_wrapper(js, js_mkfun(builtin_fs_readlink), false));

  js_set(js, realpath, "native", realpath);
  js_set(js, lib, "realpath", realpath);
}

static ant_value_t fs_make_constants(ant_t *js) {
  ant_value_t constants = js_newobj(js);
  js_set(js, constants, "F_OK", js_mknum(F_OK));
  js_set(js, constants, "R_OK", js_mknum(R_OK));
  js_set(js, constants, "W_OK", js_mknum(W_OK));
  js_set(js, constants, "X_OK", js_mknum(X_OK));
  js_set(js, constants, "O_RDONLY", js_mknum(O_RDONLY));
  js_set(js, constants, "O_WRONLY", js_mknum(O_WRONLY));
  js_set(js, constants, "O_RDWR", js_mknum(O_RDWR));
  js_set(js, constants, "O_CREAT", js_mknum(O_CREAT));
  js_set(js, constants, "O_EXCL", js_mknum(O_EXCL));
  js_set(js, constants, "O_TRUNC", js_mknum(O_TRUNC));
  js_set(js, constants, "O_APPEND", js_mknum(O_APPEND));
#ifdef O_SYMLINK
  js_set(js, constants, "O_SYMLINK", js_mknum(O_SYMLINK));
#endif
#ifdef O_NOFOLLOW
  js_set(js, constants, "O_NOFOLLOW", js_mknum(O_NOFOLLOW));
#endif
#ifdef S_IFMT
  js_set(js, constants, "S_IFMT", js_mknum(S_IFMT));
#endif
#ifdef S_IFREG
  js_set(js, constants, "S_IFREG", js_mknum(S_IFREG));
#endif
#ifdef S_IFDIR
  js_set(js, constants, "S_IFDIR", js_mknum(S_IFDIR));
#endif
#ifdef S_IFLNK
  js_set(js, constants, "S_IFLNK", js_mknum(S_IFLNK));
#endif
  return constants;
}

static ant_value_t builtin_fs_promises_getter(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t getter_fn = js_getcurrentfunc(js);
  ant_value_t cached = js_get_slot(getter_fn, SLOT_DATA);
  if (is_object_type(cached)) return cached;

  ant_value_t promises = fs_promises_library(js);
  js_set_slot(getter_fn, SLOT_DATA, promises);
  return promises;
}

ant_value_t fs_library(ant_t *js) {
  ant_value_t lib = js_mkobj(js);
  ant_value_t realpath_sync = js_heavy_mkfun(js, builtin_fs_realpathSync, js_mkundef());
  
  fs_set_callback_compatible_methods(js, lib);
  fs_init_watch_constructors(js);
  fs_init_stream_constructors(js);

  js_set(js, lib, "read", js_mkfun(builtin_fs_read_fd));
  js_set(js, lib, "readFileSync", js_mkfun(builtin_fs_readFileSync));
  js_set(js, lib, "readSync", js_mkfun(builtin_fs_readSync));
  js_set(js, lib, "stream", js_mkfun(builtin_fs_readBytes));
  js_set(js, lib, "createReadStream", js_mkfun(builtin_fs_createReadStream));
  js_set(js, lib, "createWriteStream", js_mkfun(builtin_fs_createWriteStream));
  js_set(js, lib, "openSync", js_mkfun(builtin_fs_openSync));
  js_set(js, lib, "closeSync", js_mkfun(builtin_fs_closeSync));
  js_set(js, lib, "writeFileSync", js_mkfun(builtin_fs_writeFileSync));
  js_set(js, lib, "writeSync", js_mkfun(builtin_fs_writeSync));
  js_set(js, lib, "writevSync", js_mkfun(builtin_fs_writevSync));
  js_set(js, lib, "appendFileSync", js_mkfun(builtin_fs_appendFileSync));
  js_set(js, lib, "cpSync", js_mkfun(builtin_fs_cpSync));
  js_set(js, lib, "copyFileSync", js_mkfun(builtin_fs_copyFileSync));
  js_set(js, lib, "renameSync", js_mkfun(builtin_fs_renameSync));
  js_set(js, lib, "rmSync", js_mkfun(builtin_fs_rmSync));
  js_set(js, lib, "unlinkSync", js_mkfun(builtin_fs_unlinkSync));
  js_set(js, lib, "mkdirSync", js_mkfun(builtin_fs_mkdirSync));
  js_set(js, lib, "mkdtempSync", js_mkfun(builtin_fs_mkdtempSync));
  js_set(js, lib, "rmdirSync", js_mkfun(builtin_fs_rmdirSync));
  js_set(js, lib, "statSync", js_mkfun(builtin_fs_statSync));
  js_set(js, lib, "lstatSync", js_mkfun(builtin_fs_lstatSync));
  js_set(js, lib, "fstatSync", js_mkfun(builtin_fs_fstatSync));
  js_set(js, lib, "utimesSync", js_mkfun(builtin_fs_utimesSync));
  js_set(js, lib, "futimesSync", js_mkfun(builtin_fs_futimesSync));
  js_set(js, lib, "existsSync", js_mkfun(builtin_fs_existsSync));
  js_set(js, lib, "accessSync", js_mkfun(builtin_fs_accessSync));
  js_set(js, lib, "chmodSync", js_mkfun(builtin_fs_chmodSync));
  js_set(js, lib, "readdirSync", js_mkfun(builtin_fs_readdirSync));
  js_set(js, lib, "realpathSync", realpath_sync);
  js_set(js, lib, "readlinkSync", js_mkfun(builtin_fs_readlinkSync));
  js_set(js, lib, "watch", js_mkfun(builtin_fs_watch));
  js_set(js, lib, "watchFile", js_mkfun(builtin_fs_watchFile));
  js_set(js, lib, "unwatchFile", js_mkfun(builtin_fs_unwatchFile));
  js_set(js, lib, "FSWatcher", g_fswatcher_ctor);
  js_set(js, lib, "ReadStream", g_readstream_ctor);
  js_set(js, lib, "WriteStream", g_writestream_ctor);
  js_set(js, realpath_sync, "native", realpath_sync);
  
  js_set_getter_desc(
    js, lib,
    "promises", 8,
    js_heavy_mkfun(js, builtin_fs_promises_getter, js_mkundef()),
    JS_DESC_E | JS_DESC_C
  );
  
  js_set(js, lib, "constants", fs_make_constants(js));
  js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "fs", 2));
  
  return lib;
}

ant_value_t fs_promises_library(ant_t *js) {
  ant_value_t lib = js_mkobj(js);

  fs_set_promise_methods(js, lib);
  js_set(js, lib, "constants", fs_make_constants(js));

  js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "fs/promises", 11));
  return lib;
}

ant_value_t fs_constants_library(ant_t *js) {
  ant_value_t constants = fs_make_constants(js);
  js_set(js, constants, "default", constants);
  js_set_slot_wb(js, constants, SLOT_DEFAULT, constants);
  js_set_sym(js, constants, get_toStringTag_sym(), js_mkstr(js, "constants", 9));
  return constants;
}

int has_pending_fs_ops(void) {
  return pending_requests && utarray_len(pending_requests) > 0;
}

void gc_mark_fs(ant_t *js, gc_mark_fn mark) {
  fs_watcher_t *watcher = NULL;

  if (g_fswatcher_proto) mark(js, g_fswatcher_proto);
  if (g_fswatcher_ctor) mark(js, g_fswatcher_ctor);
  if (g_filehandle_proto) mark(js, g_filehandle_proto);
  if (g_readstream_proto) mark(js, g_readstream_proto);
  if (g_readstream_ctor) mark(js, g_readstream_ctor);
  if (g_writestream_proto) mark(js, g_writestream_proto);
  if (g_writestream_ctor) mark(js, g_writestream_ctor);
  if (!pending_requests) return;
  
  unsigned int len = utarray_len(pending_requests);
  for (unsigned int i = 0; i < len; i++) {
    fs_request_t **reqp = (fs_request_t **)utarray_eltptr(pending_requests, i);
    if (reqp && *reqp) {
    mark(js, (*reqp)->promise);
    if (is_object_type((*reqp)->target_buffer)) mark(js, (*reqp)->target_buffer);
    if (is_callable((*reqp)->callback_fn))      mark(js, (*reqp)->callback_fn);
  }}

  for (watcher = active_watchers; watcher; watcher = watcher->next_active) {
    if (vtype(watcher->obj) == T_OBJ) mark(js, watcher->obj);
    if (vtype(watcher->callback) != T_UNDEF) mark(js, watcher->callback);
  }
}
