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

#include "ant.h"
#include "errors.h"
#include "internal.h"
#include "modules/lmdb.h"
#include "modules/buffer.h"
#include "modules/symbol.h"
#include "descriptors.h"
#include "gc/modules.h"

#include <lmdb.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

struct lmdb_env_handle {
  MDB_env *env;
  bool closed;
  bool read_only;
  char *path;
  lmdb_db_handle_t *db_head;
  lmdb_txn_handle_t *txn_head;
  lmdb_env_handle_t *next_global;
};

struct lmdb_db_handle {
  MDB_dbi dbi;
  bool closed;
  char *name;
  lmdb_env_handle_t *env;
  lmdb_db_handle_t *next_in_env;
  lmdb_db_handle_t *next_global;
};

struct lmdb_txn_handle {
  MDB_txn *txn;
  bool closed;
  bool read_only;
  lmdb_env_handle_t *env;
  lmdb_txn_handle_t *next_in_env;
  lmdb_txn_handle_t *next_global;
};

struct lmdb_env_ref {
  ant_value_t obj;
  lmdb_env_handle_t *env;
  lmdb_env_ref_t *next;
};

struct lmdb_db_ref {
  ant_value_t obj;
  lmdb_db_handle_t *db;
  lmdb_db_ref_t *next;
};

struct lmdb_txn_ref {
  ant_value_t obj;
  lmdb_txn_handle_t *txn;
  lmdb_txn_ref_t *next;
};

static lmdb_js_types_t lmdb_types = {0};
static lmdb_env_handle_t *env_handles = NULL;
static lmdb_db_handle_t *db_handles = NULL;
static lmdb_txn_handle_t *txn_handles = NULL;
static lmdb_env_ref_t *env_refs = NULL;
static lmdb_db_ref_t *db_refs = NULL;
static lmdb_txn_ref_t *txn_refs = NULL;

static ant_value_t make_env_obj(ant_t *js, lmdb_env_handle_t *env);
static ant_value_t make_db_obj(ant_t *js, lmdb_db_handle_t *db);
static ant_value_t make_txn_obj(ant_t *js, lmdb_txn_handle_t *txn);

static void list_remove_db(lmdb_env_handle_t *env, lmdb_db_handle_t *target) {
  if (!env || !target) return;
  lmdb_db_handle_t **cur = &env->db_head;
  while (*cur) {
    if (*cur == target) {
      *cur = target->next_in_env;
      target->next_in_env = NULL;
      return;
    }
    cur = &(*cur)->next_in_env;
  }
}

static void list_remove_txn(lmdb_env_handle_t *env, lmdb_txn_handle_t *target) {
  if (!env || !target) return;
  lmdb_txn_handle_t **cur = &env->txn_head;
  while (*cur) {
    if (*cur == target) {
      *cur = target->next_in_env;
      target->next_in_env = NULL;
      return;
    }
    cur = &(*cur)->next_in_env;
  }
}

static void register_env_ref(ant_value_t obj, lmdb_env_handle_t *env) {
  lmdb_env_ref_t *ref = ant_calloc(sizeof(lmdb_env_ref_t));
  if (!ref) return;
  ref->obj = obj;
  ref->env = env;
  ref->next = env_refs;
  env_refs = ref;
}

static void register_db_ref(ant_value_t obj, lmdb_db_handle_t *db) {
  lmdb_db_ref_t *ref = ant_calloc(sizeof(lmdb_db_ref_t));
  if (!ref) return;
  ref->obj = obj;
  ref->db = db;
  ref->next = db_refs;
  db_refs = ref;
}

static void register_txn_ref(ant_value_t obj, lmdb_txn_handle_t *txn) {
  lmdb_txn_ref_t *ref = ant_calloc(sizeof(lmdb_txn_ref_t));
  if (!ref) return;
  ref->obj = obj;
  ref->txn = txn;
  ref->next = txn_refs;
  txn_refs = ref;
}

static lmdb_env_handle_t *find_env_by_obj(ant_value_t obj) {
  for (lmdb_env_ref_t *ref = env_refs; ref; ref = ref->next)
    if (ref->obj == obj) return ref->env;
  return NULL;
}

static lmdb_db_handle_t *find_db_by_obj(ant_value_t obj) {
  for (lmdb_db_ref_t *ref = db_refs; ref; ref = ref->next)
    if (ref->obj == obj) return ref->db;
  return NULL;
}

static lmdb_txn_handle_t *find_txn_by_obj(ant_value_t obj) {
  for (lmdb_txn_ref_t *ref = txn_refs; ref; ref = ref->next)
    if (ref->obj == obj) return ref->txn;
  return NULL;
}

static void unregister_env_ref_by_obj(ant_value_t obj) {
  lmdb_env_ref_t **cur = &env_refs;
  while (*cur) {
    if ((*cur)->obj == obj) {
      lmdb_env_ref_t *next = (*cur)->next;
      free(*cur);
      *cur = next;
      return;
    }
    cur = &(*cur)->next;
  }
}

static void unregister_db_ref_by_obj(ant_value_t obj) {
  lmdb_db_ref_t **cur = &db_refs;
  while (*cur) {
    if ((*cur)->obj == obj) {
      lmdb_db_ref_t *next = (*cur)->next;
      free(*cur);
      *cur = next;
      return;
    }
    cur = &(*cur)->next;
  }
}

static void unregister_txn_ref_by_obj(ant_value_t obj) {
  lmdb_txn_ref_t **cur = &txn_refs;
  while (*cur) {
    if ((*cur)->obj == obj) {
      lmdb_txn_ref_t *next = (*cur)->next;
      free(*cur);
      *cur = next;
      return;
    }
    cur = &(*cur)->next;
  }
}

static void unregister_db_refs_by_env(lmdb_env_handle_t *env) {
  lmdb_db_ref_t **cur = &db_refs;
  while (*cur) {
    if ((*cur)->db && (*cur)->db->env == env) {
      lmdb_db_ref_t *next = (*cur)->next;
      free(*cur);
      *cur = next;
      continue;
    }
    cur = &(*cur)->next;
  }
}

static void unregister_txn_refs_by_env(lmdb_env_handle_t *env) {
  lmdb_txn_ref_t **cur = &txn_refs;
  while (*cur) {
    if ((*cur)->txn && (*cur)->txn->env == env) {
      lmdb_txn_ref_t *next = (*cur)->next;
      free(*cur);
      *cur = next;
      continue;
    }
    cur = &(*cur)->next;
  }
}

static void env_handle_close(lmdb_env_handle_t *env) {
  if (!env || env->closed) return;

  lmdb_txn_handle_t *txn = env->txn_head;
  while (txn) {
    if (!txn->closed && txn->txn) {
      mdb_txn_abort(txn->txn);
      txn->txn = NULL;
      txn->closed = true;
    }
    txn = txn->next_in_env;
  }

  lmdb_db_handle_t *db = env->db_head;
  while (db) {
    if (!db->closed) {
      mdb_dbi_close(env->env, db->dbi);
      db->closed = true;
    }
    db = db->next_in_env;
  }

  mdb_env_close(env->env);
  env->env = NULL;
  env->closed = true;
  env->db_head = NULL;
  env->txn_head = NULL;

  unregister_db_refs_by_env(env);
  unregister_txn_refs_by_env(env);
}

static lmdb_env_handle_t *get_env_handle(ant_t *js, ant_value_t obj, bool open_required) {
  lmdb_env_handle_t *env = find_env_by_obj(obj);
  if (!env) return NULL;
  if (open_required && env->closed) return NULL;
  return env;
}

static lmdb_db_handle_t *get_db_handle(ant_t *js, ant_value_t obj, bool open_required) {
  lmdb_db_handle_t *db = find_db_by_obj(obj);
  if (!db) return NULL;
  if (open_required && (db->closed || !db->env || db->env->closed)) return NULL;
  return db;
}

static lmdb_txn_handle_t *get_txn_handle(ant_t *js, ant_value_t obj, bool open_required) {
  lmdb_txn_handle_t *txn = find_txn_by_obj(obj);
  if (!txn) return NULL;
  if (open_required && (txn->closed || !txn->txn)) return NULL;
  return txn;
}

static bool option_bool(ant_t *js, ant_value_t options, const char *key, bool fallback) {
  if (vtype(options) != T_OBJ) return fallback;
  ant_value_t val = js_get(js, options, key);
  if (vtype(val) == T_UNDEF) return fallback;
  return js_truthy(js, val);
}

static unsigned int option_uint(ant_t *js, ant_value_t options, const char *key, unsigned int fallback) {
  if (vtype(options) != T_OBJ) return fallback;
  ant_value_t val = js_get(js, options, key);
  if (vtype(val) != T_NUM) return fallback;
  double n = js_getnum(val);
  if (n < 0.0) return fallback;
  return (unsigned int)n;
}

static size_t option_size(ant_t *js, ant_value_t options, const char *key, size_t fallback) {
  if (vtype(options) != T_OBJ) return fallback;
  ant_value_t val = js_get(js, options, key);
  if (vtype(val) != T_NUM) return fallback;
  double n = js_getnum(val);
  if (n < 0.0) return fallback;
  return (size_t)n;
}

static bool js_to_mdb_val(ant_t *js, ant_value_t input, MDB_val *out) {
  if (vtype(input) == T_STR) {
    size_t len = 0;
    const char *str = js_getstr(js, input, &len);
    if (!str) return false;
    out->mv_data = (void *)str;
    out->mv_size = len;
    return true;
  }

  if (vtype(input) == T_OBJ) {
    TypedArrayData *ta = buffer_get_typedarray_data(input);
    if (ta) {
      if (!ta->buffer || !ta->buffer->data) return false;
      out->mv_data = (void *)(ta->buffer->data + ta->byte_offset);
      out->mv_size = ta->byte_length;
      return true;
    }
    
    ArrayBufferData *ab = buffer_get_arraybuffer_data(input);
    if (ab) {
      if (!ab->data) return false;
      out->mv_data = (void *)ab->data;
      out->mv_size = ab->length;
      return true;
    }
  }

  return false;
}

static ant_value_t mdb_val_to_js(ant_t *js, MDB_val *val, bool as_string) {
  if (as_string) {
    return js_mkstr(js, val->mv_data, val->mv_size);
  }

  ArrayBufferData *ab = create_array_buffer_data(val->mv_size);
  if (!ab) return js_mkerr(js, "Failed to allocate LMDB read buffer");

  if (val->mv_size > 0 && val->mv_data) {
    memcpy(ab->data, val->mv_data, val->mv_size);
  }

  ant_value_t out = create_typed_array(js, TYPED_ARRAY_UINT8, ab, 0, ab->length, "Uint8Array");
  if (vtype(out) == T_ERR) free_array_buffer_data(ab);
  return out;
}

static bool parse_get_encoding(ant_t *js, ant_value_t encoding, bool *as_string) {
  if (vtype(encoding) == T_UNDEF) return true;
  if (vtype(encoding) != T_STR) return false;

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

  if (len == 5 && memcmp(mode, "bytes", 5) == 0) {
    *as_string = false;
    return true;
  }
  if (len == 4 && memcmp(mode, "utf8", 4) == 0) {
    *as_string = true;
    return true;
  }

  return false;
}

static ant_value_t lmdb_open(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1 || vtype(args[0]) != T_STR) {
    return js_mkerr(js, "lmdb.open(path, options?) requires a string path");
  }

  size_t path_len = 0;
  const char *path = js_getstr(js, args[0], &path_len);
  if (!path || path_len == 0) {
    return js_mkerr(js, "lmdb.open path cannot be empty");
  }

  ant_value_t options = nargs > 1 ? args[1] : js_mkundef();

  bool read_only = option_bool(js, options, "readOnly", false);
  bool no_subdir = option_bool(js, options, "noSubdir", false);
  bool no_sync = option_bool(js, options, "noSync", false);
  bool no_meta_sync = option_bool(js, options, "noMetaSync", false);
  bool no_read_ahead = option_bool(js, options, "noReadAhead", false);
  bool no_lock = option_bool(js, options, "noLock", false);
  bool write_map = option_bool(js, options, "writeMap", false);
  bool map_async = option_bool(js, options, "mapAsync", false);

  size_t map_size = option_size(js, options, "mapSize", 0);
  unsigned int max_readers = option_uint(js, options, "maxReaders", 0);
  unsigned int max_dbs = option_uint(js, options, "maxDbs", 0);
  unsigned int mode = option_uint(js, options, "mode", 0644U);

  unsigned int flags = 0;
  if (read_only) flags |= MDB_RDONLY;
  if (no_subdir) flags |= MDB_NOSUBDIR;
  if (no_sync) flags |= MDB_NOSYNC;
  if (no_meta_sync) flags |= MDB_NOMETASYNC;
  if (no_read_ahead) flags |= MDB_NORDAHEAD;
  if (no_lock) flags |= MDB_NOLOCK;
  if (write_map) flags |= MDB_WRITEMAP;
  if (map_async) flags |= MDB_MAPASYNC;

  MDB_env *env = NULL;
  int rc = mdb_env_create(&env);
  if (rc != 0) return js_mkerr(js, "lmdb_env_create failed: %s", mdb_strerror(rc));

  if (map_size > 0) {
    rc = mdb_env_set_mapsize(env, map_size);
    if (rc != 0) {
      mdb_env_close(env);
      return js_mkerr(js, "lmdb_env_set_mapsize failed: %s", mdb_strerror(rc));
    }
  }

  if (max_readers > 0) {
    rc = mdb_env_set_maxreaders(env, max_readers);
    if (rc != 0) {
      mdb_env_close(env);
      return js_mkerr(js, "lmdb_env_set_maxreaders failed: %s", mdb_strerror(rc));
    }
  }

  if (max_dbs > 0) {
    rc = mdb_env_set_maxdbs(env, max_dbs);
    if (rc != 0) {
      mdb_env_close(env);
      return js_mkerr(js, "lmdb_env_set_maxdbs failed: %s", mdb_strerror(rc));
    }
  }

  rc = mdb_env_open(env, path, flags, (mdb_mode_t)mode);
  if (rc != 0) {
    mdb_env_close(env);
    return js_mkerr(js, "lmdb_env_open('%s') failed: %s", path, mdb_strerror(rc));
  }

  lmdb_env_handle_t *handle = ant_calloc(sizeof(lmdb_env_handle_t));
  if (!handle) {
    mdb_env_close(env);
    return js_mkerr(js, "Out of memory");
  }

  handle->env = env;
  handle->closed = false;
  handle->read_only = read_only;
  handle->path = strndup(path, path_len);
  handle->next_global = env_handles;
  env_handles = handle;

  return make_env_obj(js, handle);
}

static ant_value_t lmdb_env_open_db(ant_t *js, ant_value_t *args, int nargs) {
  lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true);
  if (!env) return js_mkerr(js, "Invalid or closed LMDB env");

  const char *name = NULL;
  size_t name_len = 0;
  ant_value_t options = js_mkundef();

  if (nargs > 0 && vtype(args[0]) == T_STR) {
    name = js_getstr(js, args[0], &name_len);
    if (nargs > 1) options = args[1];
  } else if (nargs > 0 && vtype(args[0]) == T_OBJ) {
    options = args[0];
  }

  bool create = option_bool(js, options, "create", false);
  bool dup_sort = option_bool(js, options, "dupSort", false);
  bool dup_fixed = option_bool(js, options, "dupFixed", false);
  bool integer_key = option_bool(js, options, "integerKey", false);
  bool integer_dup = option_bool(js, options, "integerDup", false);

  if (env->read_only && create) {
    return js_mkerr(js, "Cannot open DB with create=true on a read-only env");
  }

  unsigned int dbi_flags = 0;
  if (create) dbi_flags |= MDB_CREATE;
  if (dup_sort) dbi_flags |= MDB_DUPSORT;
  if (dup_fixed) dbi_flags |= MDB_DUPFIXED;
  if (integer_key) dbi_flags |= MDB_INTEGERKEY;
  if (integer_dup) dbi_flags |= MDB_INTEGERDUP;

  unsigned int txn_flags = env->read_only ? MDB_RDONLY : 0U;
  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(env->env, NULL, txn_flags, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  MDB_dbi dbi = 0;
  rc = mdb_dbi_open(txn, name, dbi_flags, &dbi);
  if (rc != 0) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "lmdb_dbi_open failed: %s", mdb_strerror(rc));
  }

  if (txn_flags & MDB_RDONLY) {
    mdb_txn_abort(txn);
  } else {
    rc = mdb_txn_commit(txn);
    if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc));
  }

  lmdb_db_handle_t *db = ant_calloc(sizeof(lmdb_db_handle_t));
  if (!db) {
    mdb_dbi_close(env->env, dbi);
    return js_mkerr(js, "Out of memory");
  }

  db->dbi = dbi;
  db->closed = false;
  db->env = env;
  db->name = name ? strndup(name, name_len) : NULL;

  db->next_in_env = env->db_head;
  env->db_head = db;
  db->next_global = db_handles;
  db_handles = db;

  return make_db_obj(js, db);
}

static ant_value_t lmdb_env_begin_txn(ant_t *js, ant_value_t *args, int nargs) {
  lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true);
  if (!env) return js_mkerr(js, "Invalid or closed LMDB env");

  ant_value_t options = nargs > 0 ? args[0] : js_mkundef();
  bool read_only = option_bool(js, options, "readOnly", env->read_only);

  if (env->read_only && !read_only) {
    return js_mkerr(js, "Cannot create read-write transaction on read-only env");
  }

  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(env->env, NULL, read_only ? MDB_RDONLY : 0U, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  lmdb_txn_handle_t *handle = ant_calloc(sizeof(lmdb_txn_handle_t));
  if (!handle) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "Out of memory");
  }

  handle->txn = txn;
  handle->closed = false;
  handle->read_only = read_only;
  handle->env = env;

  handle->next_in_env = env->txn_head;
  env->txn_head = handle;
  handle->next_global = txn_handles;
  txn_handles = handle;

  return make_txn_obj(js, handle);
}

static ant_value_t lmdb_env_close_method(ant_t *js, ant_value_t *args, int nargs) {
  (void)args;
  (void)nargs;
  ant_value_t self = js_getthis(js);
  lmdb_env_handle_t *env = get_env_handle(js, self, false);
  if (!env) return js_mkerr(js, "Invalid LMDB env");

  env_handle_close(env);
  unregister_env_ref_by_obj(self);
  return js_mkundef();
}

static ant_value_t lmdb_env_sync_method(ant_t *js, ant_value_t *args, int nargs) {
  lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true);
  if (!env) return js_mkerr(js, "Invalid or closed LMDB env");

  bool force = true;
  if (nargs > 0) force = js_truthy(js, args[0]);

  int rc = mdb_env_sync(env->env, force ? 1 : 0);
  if (rc != 0) return js_mkerr(js, "lmdb_env_sync failed: %s", mdb_strerror(rc));
  return js_mkundef();
}

static ant_value_t lmdb_env_stat_method(ant_t *js, ant_value_t *args, int nargs) {
  lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true);
  if (!env) return js_mkerr(js, "Invalid or closed LMDB env");

  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(env->env, NULL, MDB_RDONLY, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  MDB_stat stat;
  rc = mdb_stat(txn, 0, &stat);
  mdb_txn_abort(txn);
  if (rc != 0) return js_mkerr(js, "lmdb_stat failed: %s", mdb_strerror(rc));

  ant_value_t out = js_mkobj(js);
  js_set(js, out, "psize", js_mknum((double)stat.ms_psize));
  js_set(js, out, "depth", js_mknum((double)stat.ms_depth));
  js_set(js, out, "branchPages", js_mknum((double)stat.ms_branch_pages));
  js_set(js, out, "leafPages", js_mknum((double)stat.ms_leaf_pages));
  js_set(js, out, "overflowPages", js_mknum((double)stat.ms_overflow_pages));
  js_set(js, out, "entries", js_mknum((double)stat.ms_entries));
  
  return out;
}

static ant_value_t lmdb_env_info_method(ant_t *js, ant_value_t *args, int nargs) {
  lmdb_env_handle_t *env = get_env_handle(js, js_getthis(js), true);
  if (!env) return js_mkerr(js, "Invalid or closed LMDB env");

  MDB_envinfo info;
  int rc = mdb_env_info(env->env, &info);
  if (rc != 0) return js_mkerr(js, "lmdb_env_info failed: %s", mdb_strerror(rc));

  ant_value_t out = js_mkobj(js);
  js_set(js, out, "mapSize", js_mknum((double)info.me_mapsize));
  js_set(js, out, "lastPgNo", js_mknum((double)info.me_last_pgno));
  js_set(js, out, "lastTxnId", js_mknum((double)info.me_last_txnid));
  js_set(js, out, "maxReaders", js_mknum((double)info.me_maxreaders));
  js_set(js, out, "numReaders", js_mknum((double)info.me_numreaders));
  
  return out;
}

static ant_value_t lmdb_txn_get_impl(ant_t *js, ant_value_t *args, int nargs, bool as_string) {
  if (nargs < 2) return js_mkerr(js, "txn.getBytes/getString(db, key) requires db and key");

  lmdb_txn_handle_t *txn = get_txn_handle(js, js_getthis(js), true);
  if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction");

  lmdb_db_handle_t *db = get_db_handle(js, args[0], true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");
  if (db->env != txn->env) return js_mkerr(js, "Database and transaction belong to different envs");

  MDB_val key;
  if (!js_to_mdb_val(js, args[1], &key)) {
    return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray");
  }

  MDB_val value;
  int rc = mdb_get(txn->txn, db->dbi, &key, &value);
  if (rc == MDB_NOTFOUND) return js_mkundef();
  if (rc != 0) return js_mkerr(js, "lmdb_get failed: %s", mdb_strerror(rc));

  return mdb_val_to_js(js, &value, as_string);
}

static ant_value_t lmdb_txn_get(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "txn.get(db, key, encoding?) requires db and key");
  bool as_string = false;
  if (nargs > 2 && !parse_get_encoding(js, args[2], &as_string)) {
    return js_mkerr(js, "txn.get encoding must be 'utf8' or 'bytes'");
  }
  return lmdb_txn_get_impl(js, args, nargs, as_string);
}

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

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

static ant_value_t lmdb_txn_put(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 3) return js_mkerr(js, "txn.put(db, key, value, options?) requires db, key, and value");

  lmdb_txn_handle_t *txn = get_txn_handle(js, js_getthis(js), true);
  if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction");
  if (txn->read_only) return js_mkerr(js, "Cannot write in read-only transaction");

  lmdb_db_handle_t *db = get_db_handle(js, args[0], true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");
  if (db->env != txn->env) return js_mkerr(js, "Database and transaction belong to different envs");

  MDB_val key;
  MDB_val value;
  if (!js_to_mdb_val(js, args[1], &key)) return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray");
  if (!js_to_mdb_val(js, args[2], &value)) return js_mkerr(js, "LMDB value must be string, ArrayBuffer, or TypedArray");

  ant_value_t options = nargs > 3 ? args[3] : js_mkundef();
  unsigned int flags = 0;
  if (option_bool(js, options, "noOverwrite", false)) flags |= MDB_NOOVERWRITE;
  if (option_bool(js, options, "noDupData", false)) flags |= MDB_NODUPDATA;
  if (option_bool(js, options, "append", false)) flags |= MDB_APPEND;
  if (option_bool(js, options, "appendDup", false)) flags |= MDB_APPENDDUP;

  int rc = mdb_put(txn->txn, db->dbi, &key, &value, flags);
  if (rc == MDB_KEYEXIST) return js_false;
  if (rc != 0) return js_mkerr(js, "lmdb_put failed: %s", mdb_strerror(rc));
  return js_true;
}

static ant_value_t lmdb_txn_del(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "txn.del(db, key, value?) requires db and key");

  lmdb_txn_handle_t *txn = get_txn_handle(js, js_getthis(js), true);
  if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction");
  if (txn->read_only) return js_mkerr(js, "Cannot delete in read-only transaction");

  lmdb_db_handle_t *db = get_db_handle(js, args[0], true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");
  if (db->env != txn->env) return js_mkerr(js, "Database and transaction belong to different envs");

  MDB_val key;
  if (!js_to_mdb_val(js, args[1], &key)) return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray");

  MDB_val value;
  MDB_val *value_ptr = NULL;
  if (nargs > 2 && vtype(args[2]) != T_UNDEF) {
    if (!js_to_mdb_val(js, args[2], &value)) return js_mkerr(js, "LMDB value must be string, ArrayBuffer, or TypedArray");
    value_ptr = &value;
  }

  int rc = mdb_del(txn->txn, db->dbi, &key, value_ptr);
  if (rc == MDB_NOTFOUND) return js_false;
  if (rc != 0) return js_mkerr(js, "lmdb_del failed: %s", mdb_strerror(rc));
  return js_true;
}

static ant_value_t lmdb_txn_commit(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t self = js_getthis(js);
  lmdb_txn_handle_t *txn = get_txn_handle(js, self, true);
  if (!txn) return js_mkerr(js, "Invalid or closed LMDB transaction");

  int rc = txn->read_only ? MDB_SUCCESS : mdb_txn_commit(txn->txn);
  if (txn->read_only) mdb_txn_abort(txn->txn);

  txn->txn = NULL;
  txn->closed = true;
  if (txn->env) list_remove_txn(txn->env, txn);
  unregister_txn_ref_by_obj(self);

  if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc));
  return js_mkundef();
}

static ant_value_t lmdb_txn_abort(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t self = js_getthis(js);
  lmdb_txn_handle_t *txn = get_txn_handle(js, self, false);
  if (!txn) return js_mkerr(js, "Invalid LMDB transaction");
  if (txn->closed || !txn->txn) {
    unregister_txn_ref_by_obj(self);
    return js_mkundef();
  }

  mdb_txn_abort(txn->txn);
  txn->txn = NULL;
  txn->closed = true;
  if (txn->env) list_remove_txn(txn->env, txn);
  unregister_txn_ref_by_obj(self);
  return js_mkundef();
}

static ant_value_t lmdb_db_get_impl(ant_t *js, ant_value_t *args, int nargs, bool as_string) {
  if (nargs < 1) return js_mkerr(js, "db.getBytes/getString(key) requires key");
  lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");

  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(db->env->env, NULL, MDB_RDONLY, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  MDB_val key;
  MDB_val value;
  if (!js_to_mdb_val(js, args[0], &key)) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray");
  }

  rc = mdb_get(txn, db->dbi, &key, &value);
  if (rc == MDB_NOTFOUND) {
    mdb_txn_abort(txn);
    return js_mkundef();
  }
  if (rc != 0) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "lmdb_get failed: %s", mdb_strerror(rc));
  }

  ant_value_t out = mdb_val_to_js(js, &value, as_string);
  mdb_txn_abort(txn);
  return out;
}

static ant_value_t lmdb_db_get(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "db.get(key, encoding?) requires key");
  bool as_string = false;
  if (nargs > 1 && !parse_get_encoding(js, args[1], &as_string)) {
    return js_mkerr(js, "db.get encoding must be 'utf8' or 'bytes'");
  }
  return lmdb_db_get_impl(js, args, nargs, as_string);
}

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

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

static ant_value_t lmdb_db_put(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 2) return js_mkerr(js, "db.put(key, value, options?) requires key and value");
  lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");
  if (db->env->read_only) return js_mkerr(js, "Cannot write on read-only LMDB env");

  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  MDB_val key;
  MDB_val value;
  if (!js_to_mdb_val(js, args[0], &key) || !js_to_mdb_val(js, args[1], &value)) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "LMDB key/value must be string, ArrayBuffer, or TypedArray");
  }

  ant_value_t options = nargs > 2 ? args[2] : js_mkundef();
  unsigned int flags = 0;
  if (option_bool(js, options, "noOverwrite", false)) flags |= MDB_NOOVERWRITE;
  if (option_bool(js, options, "noDupData", false)) flags |= MDB_NODUPDATA;
  if (option_bool(js, options, "append", false)) flags |= MDB_APPEND;
  if (option_bool(js, options, "appendDup", false)) flags |= MDB_APPENDDUP;

  rc = mdb_put(txn, db->dbi, &key, &value, flags);
  if (rc == MDB_KEYEXIST) {
    mdb_txn_abort(txn);
    return js_false;
  }
  if (rc != 0) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "lmdb_put failed: %s", mdb_strerror(rc));
  }

  rc = mdb_txn_commit(txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc));
  return js_true;
}

static ant_value_t lmdb_db_del(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1) return js_mkerr(js, "db.del(key, value?) requires key");
  lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");
  if (db->env->read_only) return js_mkerr(js, "Cannot delete on read-only LMDB env");

  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  MDB_val key;
  if (!js_to_mdb_val(js, args[0], &key)) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "LMDB key must be string, ArrayBuffer, or TypedArray");
  }

  MDB_val value;
  MDB_val *value_ptr = NULL;
  if (nargs > 1 && vtype(args[1]) != T_UNDEF) {
    if (!js_to_mdb_val(js, args[1], &value)) {
      mdb_txn_abort(txn);
      return js_mkerr(js, "LMDB value must be string, ArrayBuffer, or TypedArray");
    }
    value_ptr = &value;
  }

  rc = mdb_del(txn, db->dbi, &key, value_ptr);
  if (rc == MDB_NOTFOUND) {
    mdb_txn_abort(txn);
    return js_false;
  }
  if (rc != 0) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "lmdb_del failed: %s", mdb_strerror(rc));
  }

  rc = mdb_txn_commit(txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc));
  return js_true;
}

static ant_value_t lmdb_db_clear(ant_t *js, ant_value_t *args, int nargs) {
  lmdb_db_handle_t *db = get_db_handle(js, js_getthis(js), true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");
  if (db->env->read_only) return js_mkerr(js, "Cannot clear on read-only LMDB env");

  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  rc = mdb_drop(txn, db->dbi, 0);
  if (rc != 0) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "lmdb_drop(clear) failed: %s", mdb_strerror(rc));
  }

  rc = mdb_txn_commit(txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc));
  return js_mkundef();
}

static ant_value_t lmdb_db_drop(ant_t *js, ant_value_t *args, int nargs) {
  ant_value_t self = js_getthis(js);
  lmdb_db_handle_t *db = get_db_handle(js, self, true);
  if (!db) return js_mkerr(js, "Invalid or closed LMDB database handle");
  if (db->env->read_only) return js_mkerr(js, "Cannot drop on read-only LMDB env");

  bool del_db = true;
  if (nargs > 0 && vtype(args[0]) == T_OBJ) {
    del_db = option_bool(js, args[0], "delete", true);
  } else if (nargs > 0 && vtype(args[0]) != T_UNDEF) {
    del_db = js_truthy(js, args[0]);
  }

  MDB_txn *txn = NULL;
  int rc = mdb_txn_begin(db->env->env, NULL, 0, &txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_begin failed: %s", mdb_strerror(rc));

  rc = mdb_drop(txn, db->dbi, del_db ? 1 : 0);
  if (rc != 0) {
    mdb_txn_abort(txn);
    return js_mkerr(js, "lmdb_drop failed: %s", mdb_strerror(rc));
  }

  rc = mdb_txn_commit(txn);
  if (rc != 0) return js_mkerr(js, "lmdb_txn_commit failed: %s", mdb_strerror(rc));

  if (del_db) {
    db->closed = true;
    if (db->env) list_remove_db(db->env, db);
    unregister_db_ref_by_obj(self);
  }
  return js_mkundef();
}

static ant_value_t lmdb_db_close(ant_t *js, ant_value_t *args, int nargs) {
  (void)args;
  (void)nargs;
  ant_value_t self = js_getthis(js);
  lmdb_db_handle_t *db = get_db_handle(js, self, false);
  if (!db) return js_mkerr(js, "Invalid LMDB database handle");
  if (db->closed) {
    unregister_db_ref_by_obj(self);
    return js_mkundef();
  }

  if (db->env && !db->env->closed) {
    mdb_dbi_close(db->env->env, db->dbi);
    list_remove_db(db->env, db);
  }

  db->closed = true;
  unregister_db_ref_by_obj(self);
  return js_mkundef();
}

static ant_value_t lmdb_strerror_fn(ant_t *js, ant_value_t *args, int nargs) {
  if (nargs < 1 || vtype(args[0]) != T_NUM) {
    return js_mkerr(js, "lmdb.strerror(code) requires a numeric code");
  }
  int code = (int)js_getnum(args[0]);
  const char *err = mdb_strerror(code);
  return js_mkstr(js, err, strlen(err));
}

static ant_value_t lmdb_env_constructor(ant_t *js, ant_value_t *args, int nargs) {
  return js_mkerr(js, "LMDBEnv cannot be constructed directly; use lmdb.open()");
}

static ant_value_t lmdb_db_constructor(ant_t *js, ant_value_t *args, int nargs) {
  return js_mkerr(js, "LMDBDatabase cannot be constructed directly; use env.openDB()");
}

static ant_value_t lmdb_txn_constructor(ant_t *js, ant_value_t *args, int nargs) {
  return js_mkerr(js, "LMDBTxn cannot be constructed directly; use env.beginTxn()");
}

static void ensure_lmdb_prototypes(ant_t *js) {
  if (lmdb_types.ready) return;
  ant_value_t object_proto = js->sym.object_proto;

  ant_value_t env_ctor_obj = js_mkobj(js);
  ant_value_t env_proto = js_mkobj(js);
  
  js_set_proto_init(env_proto, object_proto);
  js_set(js, env_proto, "openDB", js_mkfun(lmdb_env_open_db));
  js_set(js, env_proto, "beginTxn", js_mkfun(lmdb_env_begin_txn));
  js_set(js, env_proto, "close", js_mkfun(lmdb_env_close_method));
  js_set(js, env_proto, "sync", js_mkfun(lmdb_env_sync_method));
  js_set(js, env_proto, "stat", js_mkfun(lmdb_env_stat_method));
  js_set(js, env_proto, "info", js_mkfun(lmdb_env_info_method));
  js_set_sym(js, env_proto, get_toStringTag_sym(), js_mkstr(js, "LMDBEnv", 7));
  js_set_slot(env_ctor_obj, SLOT_CFUNC, js_mkfun(lmdb_env_constructor));
  js_mkprop_fast(js, env_ctor_obj, "prototype", 9, env_proto);
  js_mkprop_fast(js, env_ctor_obj, "name", 4, ANT_STRING("LMDBEnv"));
  js_set_descriptor(js, env_ctor_obj, "name", 4, 0);

  ant_value_t db_ctor_obj = js_mkobj(js);
  ant_value_t db_proto = js_mkobj(js);
  
  js_set_proto_init(db_proto, object_proto);
  js_set(js, db_proto, "get", js_mkfun(lmdb_db_get));
  js_set(js, db_proto, "getBytes", js_mkfun(lmdb_db_get_bytes));
  js_set(js, db_proto, "getString", js_mkfun(lmdb_db_get_string));
  js_set(js, db_proto, "put", js_mkfun(lmdb_db_put));
  js_set(js, db_proto, "del", js_mkfun(lmdb_db_del));
  js_set(js, db_proto, "clear", js_mkfun(lmdb_db_clear));
  js_set(js, db_proto, "drop", js_mkfun(lmdb_db_drop));
  js_set(js, db_proto, "close", js_mkfun(lmdb_db_close));
  js_set_sym(js, db_proto, get_toStringTag_sym(), js_mkstr(js, "LMDBDatabase", 12));
  js_set_slot(db_ctor_obj, SLOT_CFUNC, js_mkfun(lmdb_db_constructor));
  js_mkprop_fast(js, db_ctor_obj, "prototype", 9, db_proto);
  js_mkprop_fast(js, db_ctor_obj, "name", 4, ANT_STRING("LMDBDatabase"));
  js_set_descriptor(js, db_ctor_obj, "name", 4, 0);

  ant_value_t txn_ctor_obj = js_mkobj(js);
  ant_value_t txn_proto = js_mkobj(js);
  
  js_set_proto_init(txn_proto, object_proto);
  js_set(js, txn_proto, "get", js_mkfun(lmdb_txn_get));
  js_set(js, txn_proto, "getBytes", js_mkfun(lmdb_txn_get_bytes));
  js_set(js, txn_proto, "getString", js_mkfun(lmdb_txn_get_string));
  js_set(js, txn_proto, "put", js_mkfun(lmdb_txn_put));
  js_set(js, txn_proto, "del", js_mkfun(lmdb_txn_del));
  js_set(js, txn_proto, "commit", js_mkfun(lmdb_txn_commit));
  js_set(js, txn_proto, "abort", js_mkfun(lmdb_txn_abort));
  js_set_sym(js, txn_proto, get_toStringTag_sym(), js_mkstr(js, "LMDBTxn", 7));
  js_set_slot(txn_ctor_obj, SLOT_CFUNC, js_mkfun(lmdb_txn_constructor));
  js_mkprop_fast(js, txn_ctor_obj, "prototype", 9, txn_proto);
  js_mkprop_fast(js, txn_ctor_obj, "name", 4, ANT_STRING("LMDBTxn"));
  js_set_descriptor(js, txn_ctor_obj, "name", 4, 0);

  lmdb_types.env_ctor = js_obj_to_func(env_ctor_obj);
  lmdb_types.db_ctor = js_obj_to_func(db_ctor_obj);
  lmdb_types.txn_ctor = js_obj_to_func(txn_ctor_obj);
  lmdb_types.env_proto = env_proto;
  lmdb_types.db_proto = db_proto;
  lmdb_types.txn_proto = txn_proto;
  lmdb_types.ready = true;
}

static ant_value_t make_env_obj(ant_t *js, lmdb_env_handle_t *env) {
  ensure_lmdb_prototypes(js);
  ant_value_t obj = js_mkobj(js);
  js_set_slot(obj, SLOT_DATA, ANT_PTR(env));
  register_env_ref(obj, env);
  if (is_special_object(lmdb_types.env_proto)) js_set_proto_init(obj, lmdb_types.env_proto);
  return obj;
}

static ant_value_t make_db_obj(ant_t *js, lmdb_db_handle_t *db) {
  ensure_lmdb_prototypes(js);
  ant_value_t obj = js_mkobj(js);
  js_set_slot(obj, SLOT_DATA, ANT_PTR(db));
  register_db_ref(obj, db);
  if (is_special_object(lmdb_types.db_proto)) js_set_proto_init(obj, lmdb_types.db_proto);
  return obj;
}

static ant_value_t make_txn_obj(ant_t *js, lmdb_txn_handle_t *txn) {
  ensure_lmdb_prototypes(js);
  ant_value_t obj = js_mkobj(js);
  js_set_slot(obj, SLOT_DATA, ANT_PTR(txn));
  register_txn_ref(obj, txn);
  if (is_special_object(lmdb_types.txn_proto)) js_set_proto_init(obj, lmdb_types.txn_proto);
  return obj;
}

ant_value_t lmdb_library(ant_t *js) {
  ensure_lmdb_prototypes(js);
  ant_value_t lib = js_mkobj(js);
  js_set(js, lib, "open", js_mkfun(lmdb_open));
  js_set(js, lib, "strerror", js_mkfun(lmdb_strerror_fn));
  js_set(js, lib, "Env", lmdb_types.env_ctor);
  js_set(js, lib, "Database", lmdb_types.db_ctor);
  js_set(js, lib, "Txn", lmdb_types.txn_ctor);

  int major = 0; int minor = 0; int patch = 0;
  const char *version = mdb_version(&major, &minor, &patch);

  js_set(js, lib, "version", js_mkstr(js, version, strlen(version)));
  js_set(js, lib, "versionMajor", js_mknum((double)major));
  js_set(js, lib, "versionMinor", js_mknum((double)minor));
  js_set(js, lib, "versionPatch", js_mknum((double)patch));

  ant_value_t constants = js_mkobj(js);
  js_set(js, constants, "NOOVERWRITE", js_mknum((double)MDB_NOOVERWRITE));
  js_set(js, constants, "NODUPDATA", js_mknum((double)MDB_NODUPDATA));
  js_set(js, constants, "APPEND", js_mknum((double)MDB_APPEND));
  js_set(js, constants, "APPENDDUP", js_mknum((double)MDB_APPENDDUP));
  js_set(js, constants, "NOSUBDIR", js_mknum((double)MDB_NOSUBDIR));
  js_set(js, constants, "NOSYNC", js_mknum((double)MDB_NOSYNC));
  js_set(js, constants, "NOMETASYNC", js_mknum((double)MDB_NOMETASYNC));
  js_set(js, constants, "WRITEMAP", js_mknum((double)MDB_WRITEMAP));
  js_set(js, constants, "MAPASYNC", js_mknum((double)MDB_MAPASYNC));
  js_set(js, constants, "NOTLS", js_mknum((double)MDB_NOTLS));
  js_set(js, constants, "NOLOCK", js_mknum((double)MDB_NOLOCK));
  js_set(js, constants, "NORDAHEAD", js_mknum((double)MDB_NORDAHEAD));
  js_set(js, constants, "NOMEMINIT", js_mknum((double)MDB_NOMEMINIT));
  js_set(js, lib, "constants", constants);

  js_set_sym(js, lib, get_toStringTag_sym(), js_mkstr(js, "lmdb", 4));
  return lib;
}

void gc_mark_lmdb(ant_t *js, gc_mark_fn mark) {
  if (lmdb_types.ready) {
    mark(js, lmdb_types.env_ctor);
    mark(js, lmdb_types.db_ctor);
    mark(js, lmdb_types.txn_ctor);
    mark(js, lmdb_types.env_proto);
    mark(js, lmdb_types.db_proto);
    mark(js, lmdb_types.txn_proto);
  }
  for (lmdb_env_ref_t *ref = env_refs; ref; ref = ref->next) mark(js, ref->obj);
  for (lmdb_db_ref_t *ref = db_refs; ref; ref = ref->next) mark(js, ref->obj);
  for (lmdb_txn_ref_t *ref = txn_refs; ref; ref = ref->next) mark(js, ref->obj);
}

void cleanup_lmdb_module(void) {
  lmdb_txn_handle_t *txn = txn_handles;
  while (txn) {
    if (!txn->closed && txn->txn) {
      mdb_txn_abort(txn->txn);
      txn->txn = NULL;
      txn->closed = true;
    }
    txn = txn->next_global;
  }

  lmdb_db_handle_t *db = db_handles;
  while (db) {
    if (!db->closed && db->env && !db->env->closed) {
      mdb_dbi_close(db->env->env, db->dbi);
      db->closed = true;
    }
    db = db->next_global;
  }

  lmdb_env_handle_t *env = env_handles;
  while (env) {
    if (!env->closed) env_handle_close(env);
    env = env->next_global;
  }

  txn = txn_handles;
  while (txn) {
    lmdb_txn_handle_t *next = txn->next_global;
    free(txn);
    txn = next;
  }
  txn_handles = NULL;

  db = db_handles;
  while (db) {
    lmdb_db_handle_t *next = db->next_global;
    free(db->name);
    free(db);
    db = next;
  }
  db_handles = NULL;

  env = env_handles;
  while (env) {
    lmdb_env_handle_t *next = env->next_global;
    free(env->path);
    free(env);
    env = next;
  }
  env_handles = NULL;

  while (env_refs) {
    lmdb_env_ref_t *next = env_refs->next;
    free(env_refs);
    env_refs = next;
  }

  while (db_refs) {
    lmdb_db_ref_t *next = db_refs->next;
    free(db_refs);
    db_refs = next;
  }

  while (txn_refs) {
    lmdb_txn_ref_t *next = txn_refs->next;
    free(txn_refs);
    txn_refs = next;
  }

  lmdb_types = (lmdb_js_types_t){0};
}
